golang:为什么不直接在rpc server reuse instance中的free list

golang: Why not the free list in rpc server reuse instance directly

net/rpc 包中的

RPC server 包含 Request 结构和 Response 结构的两个免费列表。 Request 结构通过其 next 字段维护此列表。

// Server represents an RPC Server.
type Server struct {
    // ...
    freeReq    *Request // header node of Request free list
    freeResp   *Response // header node of Response free list
}

type Request struct {
    ServiceMethod string   // format: "Service.Method"
    Seq           uint64   // sequence number chosen by client
    next          *Request // for free list in Server
}

rpc server 中的空闲列表似乎是一个对象池。处理rpc请求时,服务器调用getRequest从空闲列表中获取一个请求实例。处理请求后,服务器调用 freeRequest 将请求实例放回空闲列表。

func (server *Server) getRequest() *Request {
    server.reqLock.Lock()
    req := server.freeReq
    if req == nil {
        req = new(Request) // free list is empty
    } else {
        server.freeReq = req.next // free list isn't empty
        *req = Request{} // Why not reuse instance directly?
    }
    server.reqLock.Unlock()
    return req
}

func (server *Server) freeRequest(req *Request) {
    server.reqLock.Lock()
    req.next = server.freeReq
    server.freeReq = req
    server.reqLock.Unlock()
}

我对 getRequest 函数感到困惑。当空闲列表为空时,它会按预期创建一个新实例。当空闲列表不为空时,它执行*req = Request{}。我认为 Request{} 也创建了一个新实例。那么持有这个空闲列表有什么意义呢?


另外,我写了一个demo来展示*req = Request{}格式语句的效果

type Student struct {
    Name string
    Age int
}

func main() {
    s := &Student{"John", 20}
    fmt.Printf("Address: %p Content: %v\n", s, s)

    *s = Student{"Frank", 18} // similar to *req = Request{}
    fmt.Printf("Address: %p Content: %v\n", s, s)
}

输出为:

Address: 0xc42000a4c0 Content: &{John 20}
Address: 0xc42000a4c0 Content: &{Frank 18}

所以语句*req = Request{}不会改变指针的地址,但会改变内容。

freelist 的想法是通过重用已经创建的对象实例来减少动态内存分配量。

它是这样工作的:

当第一个请求发出时,空闲列表是空的,因此将在堆上分配一个新的结构。当不再需要这些时,它们将被放入空闲列表以供重用。如果您没有空闲列表,那么下一个请求将需要在堆上创建新的 Request/Response 结构,一遍又一遍地执行此操作可能代价高昂。使用空闲列表可以避免这种情况,因为下一个请求可以简单地重用已经分配(和停放)的对象。

我猜你对这一行感到困惑:

*req = Request{}

反对freelist为空,在堆上创建新对象的情况req = new(Request),这不会在堆上分配对象。

它只是通过复制默认值将已经分配的对象(从空闲列表中取出)重置为默认状态。

您可以将此行分解为以下内容:

r := Request{} // Create a request with default content on stack (not heap!)
*req = r // Copy all fields from default request to req

无论在 getRequest() 中采用什么路径,它总是 returns 默认的初始化请求对象 - 没有上一个请求的遗留物。