MENU

Stealthpool, an Off Heap Golang Memory Pool

2021 年 07 月 19 日 • 阅读: 7352 • 后端

Go 语言和 Java,C# 一样,都在运行时通过垃圾收集器(Garbage Collector)管理内存。这虽然方便了程序编写,但也带来了额外开销,垃圾回收有时会导致应用响应时间过长,这也催生了 GC 调优概念。本文将介绍 Github 上的一个 Golang 非堆内存池,它借助系统调用,能获得不受 GC 控制的内存。如果你需要长期使用一块内存,这能够降低 GC 负担。

Project Structure

➜  git clone https://github.com/Link512/stealthpool.git
➜  cd stealthpool
➜  tree -P '*.go'
.
├── alloc_unix.go
├── alloc_windows.go
├── doc.go
├── errors.go
├── example_test.go
├── multierr.go
├── multierr_test.go
├── pool.go
└── pool_test.go

0 directories, 9 files

项目结构非常简单,只有一级目录,共 9 个源文件。最上面两个 alloc_ 文件封装了真正的内存分配系统调用,由于不同操作系统的 API 有所区别,所有使用两个文件。得益于 Golang 的条件编译,这些文件在编译到目标平台时会自动选择。xxx_test.go 命名的文件是对 xxx.go 的单元测试,这些文件可以通过 go test 命令编译执行。doc.go 中包含整个包的整体注解,go doc 命令会读取它生成项目文档的 Overviewerrors.go 定义了包下的所有自定义错误。multierr.go 则是多个错误的包装。pool.go 提供了项目的核心接口和实现。

Bussiness Logic

首先是物理内存申请,*nix 系统逻辑位于 alloc_unix.goWindows 位于 alloc_windows.go,两个文件内部包含的函数签名是一样的,编译器会根据目标系统任选其一。

第一个函数是 alloc,它接收一个 int 型参数,代表要分配的字节大小。返回 byte 切片,代表分配到的内存,还有一个 error 代表是否分配成功。

func alloc(size int) ([]byte, error)

第二个函数是 dealloc,它接收一个 byte 切片代表要释放的内存,返回一个 error 代表操作是否成功。它的参数应该是 alloc 的返回值。

func dealloc(b []byte) error

下面是 *nix 系统对应的源码内容。

// +build !windows !appengine

package stealthpool

import "golang.org/x/sys/unix"

func alloc(size int) ([]byte, error) {
    return unix.Mmap(
        -1,                                  // required by MAP_ANONYMOUS
        0,                                   // offset from file descriptor start, required by MAP_ANONYMOUS
        size,                                // how much memory
        unix.PROT_READ|unix.PROT_WRITE,      // protection on memory
        unix.MAP_ANONYMOUS|unix.MAP_PRIVATE, // private so other processes don't see the changes, anonymous so that nothing gets synced to the file
    )
}

func dealloc(b []byte) error {
    return unix.Munmap(b)
}

alloc 函数体直接通过 Golang 封装的 Mmap 执行 mmap 系统调用,它实际上是内存文件映射,签名如下

func Mmap(fd int, offset int64, length int, prot int, flags int) (data []byte, err error)

实际参数的含义如下

参数含义
-1映射的文件描述符。-1 用于兼容匿名映射
0映射对象内容的起点。0 代表从起始位置映射
size映射区的长度。单位字节
unix.PROT_READ / unix.PROT_WRITE期望的内存保护标志。此处表示页可读可写
unix.MAP_ANONYMOUS / unix.MAP_PRIVATE映射对象的类型。此处为匿名私有映射,映射区不与任何文件关联,内存区域的写入不会影响到原文件。不与其它进程共享映射

dealloc 函数体直接通过 Golang 封装的 Munmap 执行 munmap 系统调用,它用于取消内存文件映射,签名如下

func Munmap(b []byte) (err error)

再看 Windows 下对应的源代码内容。

Windows 下的内存申请与释放

package stealthpool

import (
    "reflect"
    "runtime"
    "syscall"
    "unsafe"
)

const (
    memCommit  = 0x1000
    memReserve = 0x2000
    memRelease = 0x8000

    pageRW = 0x04

    kernelDll   = "kernel32.dll"
    allocFunc   = "VirtualAlloc"
    deallocFunc = "VirtualFree"

    errOK = 0
)

var (
    kernel         *syscall.DLL
    virtualAlloc   *syscall.Proc
    virtualDealloc *syscall.Proc
)

func init() {
    runtime.LockOSThread()
    kernel = syscall.MustLoadDLL(kernelDll)
    virtualAlloc = kernel.MustFindProc(allocFunc)
    virtualDealloc = kernel.MustFindProc(deallocFunc)
    runtime.UnlockOSThread()
}

func alloc(size int) ([]byte, error) {
    addr, _, err := virtualAlloc.Call(uintptr(0), uintptr(size), memCommit|memReserve, pageRW)
    errNo := err.(syscall.Errno)
    if errNo != errOK {
        return nil, err
    }

    var result []byte
    hdr := (*reflect.SliceHeader)(unsafe.Pointer(&result))
    hdr.Data = addr
    hdr.Cap = size
    hdr.Len = size
    return result, nil
}

func dealloc(b []byte) error {
    hdr := (*reflect.SliceHeader)(unsafe.Pointer(&b))
    _, _, err := virtualDealloc.Call(hdr.Data, 0, memRelease)
    errNo := err.(syscall.Errno)
    if errNo != errOK {
        return err
    }

    return nil
}

Golang 并未封装 Windows 下的内存分配专用系统调用,但提供了加载动态链接库和过程函数的通用调用,使用者需要加载动态链接库和其中的过程函数,随后执行过程函数完成所需功能。下面是涉及到的类型和函数签名:

相关类型和函数签名

package runtime

/*
LockOSThread wires the calling goroutine to its current operating system thread. The calling goroutine will always execute in that thread, and no other goroutine will execute in it, until the calling goroutine has made as many calls to UnlockOSThread as to LockOSThread. If the calling goroutine exits without unlocking the thread, the thread will be terminated.

All init functions are run on the startup thread. Calling LockOSThread from an init function will cause the main function to be invoked on that thread.

A goroutine should call LockOSThread before calling OS services or non-Go library functions that depend on per-thread state.
*/
func LockOSThread()

/*
UnlockOSThread undoes an earlier call to LockOSThread. If this drops the number of active LockOSThread calls on the calling goroutine to zero, it unwires the calling goroutine from its fixed operating system thread. If there are no active LockOSThread calls, this is a no-op.

Before calling UnlockOSThread, the caller must ensure that the OS thread is suitable for running other goroutines. If the caller made any permanent changes to the state of the thread that would affect other goroutines, it should not call this function and thus leave the goroutine locked to the OS thread until the goroutine (and hence the thread) exits.
*/
func UnlockOSThread()
package syscall

/*
An Errno is an unsigned number describing an error condition. It implements the error interface. The zero Errno is by convention a non-error, so code to convert from Errno to error should use:

err = nil
if errno != 0 {
    err = errno
}

Errno values can be tested against error values from the os package using errors.Is. For example:

_, _, err := syscall.Syscall(...)
if errors.Is(err, fs.ErrNotExist) ...
*/
type Errno uintptr

// A DLL implements access to a single DLL.
type DLL struct {
    Name   string
    Handle Handle
}

// A Proc implements access to a procedure inside a DLL.
type Proc struct {
    Dll  *DLL
    Name string
    // contains filtered or unexported fields
}

/*
LoadDLL loads the named DLL file into memory.

If name is not an absolute path and is not a known system DLL used by Go, Windows will search for the named DLL in many locations, causing potential DLL preloading attacks.

Use LazyDLL in golang.org/x/sys/windows for a secure way to load system DLLs.
*/
func LoadDLL(name string) (*DLL, error)

// MustLoadDLL is like LoadDLL but panics if load operation fails.
func MustLoadDLL(name string) *DLL

// FindProc searches DLL d for procedure named name and returns *Proc if found. It returns an error if search fails.
func (d *DLL) FindProc(name string) (proc *Proc, err error)

// MustFindProc is like FindProc but panics if search fails.
func (d *DLL) MustFindProc(name string) *Proc

/* 
Call executes procedure p with arguments a. It will panic if more than 18 arguments are supplied.

The returned error is always non-nil, constructed from the result of GetLastError. Callers must inspect the primary return value to decide whether an error occurred (according to the semantics of the specific function being called) before consulting the error. The error always has type syscall.Errno.

On amd64, Call can pass and return floating-point values. To pass an argument x with C type "float", use uintptr(math.Float32bits(x)). To pass an argument with C type "double", use uintptr(math.Float64bits(x)). Floating-point return values are returned in r2. The return value for C type "float" is math.Float32frombits(uint32(r2)). For C type "double", it is math.Float64frombits(uint64(r2)).
*/
func (p *Proc) Call(a ...uintptr) (r1, r2 uintptr, lastErr error)
package builtin

// uintptr is an integer type that is large enough to hold the bit pattern of any pointer.
type uintptr uintptr
package unsafe

// ArbitraryType is here for the purposes of documentation only and is not actually part of the unsafe package. It represents the type of an arbitrary Go expression.
type ArbitraryType int

/*

Pointer represents a pointer to an arbitrary type. There are four special operations available for type Pointer that are not available for other types:

- A pointer value of any type can be converted to a Pointer.
- A Pointer can be converted to a pointer value of any type.
- A uintptr can be converted to a Pointer.
- A Pointer can be converted to a uintptr.

Pointer therefore allows a program to defeat the type system and read and write arbitrary memory. It should be used with extreme care.
*/
type Pointer *ArbitraryType
package reflect

// SliceHeader is the runtime representation of a slice. It cannot be used safely or portably and its representation may change in a later release. Moreover, the Data field is not sufficient to guarantee the data it references will not be garbage collected, so programs must keep a separate, correctly typed pointer to the underlying data.
type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

文件首先定义了一组常量,它们是系统调用要用到的动态链接库和过程名称,以及过程参数的值。它们可以在 Win32 API 文档中找到。

const (
    memCommit  = 0x1000             // 物理存储提交给保留区域。作为 VirtualAlloc 的参数
    memReserve = 0x2000             // 页面被保留以供将来使用。作为 VirtualAlloc 的参数
    memRelease = 0x8000             // 释放特定区域的页面。作为 VirtualFree 的参数

    pageRW = 0x04                   // 页面可读可写

    kernelDll   = "kernel32.dll"    // 内核动态链接库名
    allocFunc   = "VirtualAlloc"    // 页面分配过程名
    deallocFunc = "VirtualFree"     // 页面回收过程名

    errOK = 0                       // 系统调用成功的返回值
)

紧接着定义了一组变量:

var (
    kernel         *syscall.DLL     // 内核系统调用
    virtualAlloc   *syscall.Proc    // 页面分配过程
    virtualDealloc *syscall.Proc    // 页面释放过程
)

下面是初始化函数,它通过通用系统调用加载上面三个变量。该函数使用 LockOSThread()UnlockOSThread() 包裹函数体,确保 goroutine 在同一线程执行,这样才能保证系统调用成功。

func init() {
    runtime.LockOSThread()
    kernel = syscall.MustLoadDLL(kernelDll)
    virtualAlloc = kernel.MustFindProc(allocFunc)
    virtualDealloc = kernel.MustFindProc(deallocFunc)
    runtime.UnlockOSThread()
}

再往下是内存分配函数,它通过 VirtualAlloc 内核调用向操作系统申请内存,这个 Win32 API 签名如下:

// Reserves, commits, or changes the state of a region of pages in the virtual address space of the calling process. Memory allocated by this function is automatically initialized to zero.
LPVOID VirtualAlloc(
  LPVOID lpAddress,         // The starting address of the region to allocate.
  SIZE_T dwSize,            // The size of the region, in bytes.
  DWORD  flAllocationType,  // The type of memory allocation.
  DWORD  flProtect          // The memory protection for the region of pages to be allocated.
);

VirtualAlloc.Call 的第一个参数是起始地址,uintptr(0) 代表 C 中的 NULLNULL 让系统决定起始地址。第二个参数是申请的内存大小,单位字节。第三个参数是分配类型,此处为保留并映射物理内存。最后一个参数是内存保护类型,此处为可读可写。申请成功后,会把地址 addr 绑定到一个字节切片返回,借助 unsafe.Pointer 可以实现任意类型指针的转换。

func alloc(size int) ([]byte, error) {
    addr, _, err := virtualAlloc.Call(uintptr(0), uintptr(size), memCommit|memReserve, pageRW)
    errNo := err.(syscall.Errno)
    if errNo != errOK {
        return nil, err
    }

    var result []byte
    hdr := (*reflect.SliceHeader)(unsafe.Pointer(&result))
    hdr.Data = addr
    hdr.Cap = size
    hdr.Len = size
    return result, nil
}

该文件的最后一个函数是 dealloc,它通过 VirtualFree 让操作系统释放内存,这个 Win32 API 签名如下:

// Releases, decommits, or releases and decommits a region of pages within the virtual address space of the calling process.
BOOL VirtualFree(
  LPVOID lpAddress,         // A pointer to the base address of the region of pages to be freed.
  SIZE_T dwSize,            // The size of the region of memory to be freed, in bytes.
  DWORD  dwFreeType         // The type of free operation.
);
func dealloc(b []byte) error {
    hdr := (*reflect.SliceHeader)(unsafe.Pointer(&b))
    _, _, err := virtualDealloc.Call(hdr.Data, 0, memRelease)
    errNo := err.(syscall.Errno)
    if errNo != errOK {
        return err
    }

    return nil
}

VirtualDealloc.Call 的第一个参数是要释放页的基地址。第二个参数是要释放的字节数,和 memRelease 搭配使用时,必须是 0。第三个参数是释放类型,此处是直接释放。

以上是涉及操作系统的全部内容,下面全是 Go 语言的自身特性。首先看 errors.go,它定义了三个自定义错误,分别是 池满预分配越界块非法。此处主要关注错误集中定义的思想。

package stealthpool

import "errors"

var (
    // ErrPoolFull is returned when the maximum number of allocated blocks has been reached
    ErrPoolFull = errors.New("pool is full")
    // ErrPreallocOutOfBounds is returned when whe number of preallocated blocks requested is either negative or above maxBlocks
    ErrPreallocOutOfBounds = errors.New("prealloc value out of bounds")
    // ErrInvalidBlock is returned when an invalid slice is passed to Return()
    ErrInvalidBlock = errors.New("trying to return invalid block")
)

接下来是 multierr.go,它定义了一个 multiErr 结构,内部是 error 切片。之后定义了一个构造方法 newMultiErr,它返回一个类型指针。之后在 *multiErr 上定义了三个函数,其中的 Error() 使得 *multiErr 也成为 error 类型。

package stealthpool

import "strings"

type multiErr struct {
    errs []error
}

func newMultiErr() *multiErr {
    return &multiErr{}
}

func (e *multiErr) Add(err error) {
    if err != nil {
        e.errs = append(e.errs, err)
    }
}

func (e *multiErr) Return() error {
    if len(e.errs) == 0 {
        return nil
    }
    return e
}

func (e *multiErr) Error() string {
    result := strings.Builder{}
    for _, e := range e.errs {
        result.WriteString(e.Error() + "\n")
    }
    return strings.TrimRight(result.String(), "\n")
}

接下来是一个包含核心业务逻辑和接口设计的 pool.go 文件,它实现了一个内存池。

pool.go 源码

package stealthpool

import (
    "reflect"
    "runtime"
    "sync"
    "unsafe"
)

type poolOpts struct {
    blockSize int
    preAlloc  int
}

var (
    defaultPoolOpts = poolOpts{
        blockSize: 4 * 1024,
    }
)

// PoolOpt is a configuration option for a stealthpool
type PoolOpt func(*poolOpts)

// WithPreAlloc specifies how many blocks the pool should preallocate on initialization. Default is 0.
func WithPreAlloc(prealloc int) PoolOpt {
    return func(opts *poolOpts) {
        opts.preAlloc = prealloc
    }
}

// WithBlockSize specifies the block size that will be returned. It is highly advised that this block size be a multiple of 4KB or whatever value
// `os.Getpagesize()`, since the mmap syscall returns page aligned memory
func WithBlockSize(blockSize int) PoolOpt {
    return func(opts *poolOpts) {
        opts.blockSize = blockSize
    }
}

// Pool is the off heap memory pool. It it safe to be used concurrently
type Pool struct {
    sync.RWMutex

    free      [][]byte
    allocated map[*byte]struct{}
    initOpts  poolOpts
    maxBlocks int
}

// New returns a new stealthpool with the given capacity. The configuration options can be used to change how many blocks are preallocated or block size.
// If preallocation fails (out of memory, etc), a cleanup of all previously preallocated will be attempted
func New(maxBlocks int, opts ...PoolOpt) (*Pool, error) {
    o := defaultPoolOpts
    for _, opt := range opts {
        opt(&o)
    }
    p := &Pool{
        initOpts:  o,
        free:      make([][]byte, 0, maxBlocks),
        allocated: make(map[*byte]struct{}, maxBlocks),
        maxBlocks: maxBlocks,
    }
    if o.preAlloc > 0 {
        if err := p.prealloc(o.preAlloc); err != nil {
            return nil, err
        }
    }
    runtime.SetFinalizer(p, func(pool *Pool) {
        pool.Close()
    })
    return p, nil
}

// Get returns a memory block. It will first try and retrieve a previously allocated block and if that's not possible, will allocate a new block.
// If there were maxBlocks blocks already allocated, returns ErrPoolFull
func (p *Pool) Get() ([]byte, error) {
    if b, ok := p.tryPop(); ok {
        return b, nil
    }

    p.Lock()
    defer p.Unlock()

    if len(p.allocated) == p.maxBlocks {
        return nil, ErrPoolFull
    }
    result, err := alloc(p.initOpts.blockSize)
    if err != nil {
        return nil, err
    }
    k := &result[0]
    p.allocated[k] = struct{}{}
    return result, nil
}

// Return gives back a block retrieved from Get and stores it for future re-use.
// The block has to be exactly the same slice object returned from Get(), otherwise ErrInvalidBlock will be returned.
func (p *Pool) Return(b []byte) error {
    if err := p.checkValidBlock(b); err != nil {
        return err
    }
    p.Lock()
    defer p.Unlock()
    p.free = append(p.free, b)
    return nil
}

// FreeCount returns the number of free blocks that can be reused
func (p *Pool) FreeCount() int {
    p.RLock()
    defer p.RUnlock()
    return len(p.free)
}

// AllocCount returns the total number of allocated blocks so far
func (p *Pool) AllocCount() int {
    p.RLock()
    defer p.RUnlock()
    return len(p.allocated)
}

// Close will cleanup the memory pool and deallocate ALL previously allocated blocks.
// Using any of the blocks returned from Get() after a call to Close() will result in a panic
func (p *Pool) Close() error {
    return p.cleanup()
}

func (p *Pool) tryPop() ([]byte, bool) {
    p.Lock()
    defer p.Unlock()

    if len(p.free) == 0 {
        return nil, false
    }
    n := len(p.free) - 1
    result := p.free[n]
    p.free[n] = nil
    p.free = p.free[:n]
    return result, true
}

func (p *Pool) checkValidBlock(block []byte) error {
    if len(block) == 0 || len(block) != cap(block) {
        return ErrInvalidBlock
    }

    k := &block[0]
    p.RLock()
    _, found := p.allocated[k]
    p.RUnlock()

    if !found || len(block) != p.initOpts.blockSize {
        return ErrInvalidBlock
    }
    return nil
}

func (p *Pool) prealloc(n int) error {
    if n < 0 || n > p.maxBlocks {
        return ErrPreallocOutOfBounds
    }

    for i := 0; i < n; i++ {
        block, err := alloc(p.initOpts.blockSize)
        if err != nil {
            _ = p.cleanup()
            return err
        }
        k := &block[0]
        p.allocated[k] = struct{}{}
        p.free = append(p.free, block)
    }
    return nil
}

func (p *Pool) cleanup() error {
    p.Lock()
    defer p.Unlock()

    multiErr := newMultiErr()
    for arrayPtr := range p.allocated {
        var block []byte
        hdr := (*reflect.SliceHeader)(unsafe.Pointer(&block))
        hdr.Cap = p.initOpts.blockSize
        hdr.Len = p.initOpts.blockSize
        hdr.Data = uintptr(unsafe.Pointer(arrayPtr))
        if err := dealloc(block); err != nil {
            multiErr.Add(err)
        }
    }
    p.allocated = nil
    p.free = nil
    return multiErr.Return()
}

文件首先定义了一个 poolOpts 结构,表示内存池选项,包含块大小和预分配块数两个属性。紧接着定义了一个 defaultPoolOpts 变量,代表默认选项,指定了块大小为 4K

type poolOpts struct {
    blockSize int
    preAlloc  int
}

var (
    defaultPoolOpts = poolOpts{
        blockSize: 4 * 1024,
    }
)

接着,定义了一个 PoolOpt 类型,它是一个函数,接收 *poolOpts 参数,暴露为公共接口。紧接着定义了两个公共函数,它们分别接收 poolOpts 的一个属性,返回一个 PoolOpt 函数,返回的函数接收 *poolOpts,会在参数上设置相应属性,值是外层函数参数。这是一种 函数式选项设计模式,它使用了闭包,可以使下文提到的构造器参数非常灵活。

// PoolOpt is a configuration option for a stealthpool
type PoolOpt func(*poolOpts)

// WithPreAlloc specifies how many blocks the pool should preallocate on initialization. Default is 0.
func WithPreAlloc(prealloc int) PoolOpt {
    return func(opts *poolOpts) {
        opts.preAlloc = prealloc
    }
}

// WithBlockSize specifies the block size that will be returned. It is highly advised that this block size be a multiple of 4KB or whatever value
// `os.Getpagesize()`, since the mmap syscall returns page aligned memory
func WithBlockSize(blockSize int) PoolOpt {
    return func(opts *poolOpts) {
        opts.blockSize = blockSize
    }
}

再往下,定义了内存池结构和它的构造函数。该结构持有一个读写锁,确保对池的访问是线程安全的。free 属性是一个二维字节切片,第一维对应一块真正的内存空间,第二维表明有很多块。allocated 属性是一个字节指针到空结构的映射,用来记录申请到的各个物理内存块地址。maxBlocks 代表内存池中有多少块内存。

构造函数的第一个参数是 maxBlocks,指定内存池内存块个数。后面是任意多个 PoolOpt 参数,用来设定内存池属性,上文提到这种设计叫 函数式选项设计模式,它让调用者可以非常方便地构建对象。

如果指定了预分配内存,则调用 p.prealloc() 处理。函数最后通过 runtime.SetFinalizer 为内存池指定了不可达回调,这可避免使用者未关闭内存池导致的内存泄露。

// Pool is the off heap memory pool. It it safe to be used concurrently
type Pool struct {
    sync.RWMutex
    free      [][]byte
    allocated map[*byte]struct{}
    initOpts  poolOpts
    maxBlocks int
}

// New returns a new stealthpool with the given capacity. The configuration options can be used to change how many blocks are preallocated or block size.
// If preallocation fails (out of memory, etc), a cleanup of all previously preallocated will be attempted
func New(maxBlocks int, opts ...PoolOpt) (*Pool, error) {
    o := defaultPoolOpts
    for _, opt := range opts {
        opt(&o)
    }
    p := &Pool{
        initOpts:  o,
        free:      make([][]byte, 0, maxBlocks),
        allocated: make(map[*byte]struct{}, maxBlocks),
        maxBlocks: maxBlocks,
    }
    if o.preAlloc > 0 {
        if err := p.prealloc(o.preAlloc); err != nil {
            return nil, err
        }
    }
    runtime.SetFinalizer(p, func(pool *Pool) {
        pool.Close()
    })
    return p, nil
}

下面的函数都是切片、映射等结构和互斥锁的基本操作,就不一一介绍了。项目还包含编译配置和持续集成/持续交互文件,交由读者自行了解。

➜  tree -a -I '.git|*.go|go.*'
.
├── .drone.yml
├── .golangci.yml
├── LICENSE
├── Makefile
└── README.md

0 directories, 5 files

API

首先是内存池的创建,需要指定内存块个数。不要忘记使用完毕后关闭内存池,否则会造成内存泄漏,虽然构造器预留了预防措施,但那要等待一次垃圾收集。

// initialize a pool which will allocate a maximum of 100 blocks
pool, err := stealthpool.New(100)
defer pool.Close() // ALWAYS close the pool unless you're very fond of memory leaks

得益于 选项设计模式,还可以方便地指定块大小和预申请块数。

// initialize a pool with custom block size and preallocated blocks
poolCustom, err := stealthpool.New(100, stealthpool.WithBlockSize(8*1024), stealthpool.WithPreAlloc(100))
defer poolCustom.Close() // ALWAYS close the pool unless you're very fond of memory leaks

内存池创建完毕后,通过 Get 方法获取一块内存,通过 Return 方法将内存块还给内存池。

block, err := poolCustom.Get()
// do some work with block
// then return it exactly as-is to the pool
err = poolCustom.Return(block)

References

TG 大佬群 QQ 大佬群

返回文章列表 文章二维码
本页链接的二维码
打赏二维码
添加新评论

Loading captcha...

已有 2 条评论
  1. 于长野 于长野   iOS 14.6  Safari 14.1.1

    勤奋的logi::quyin:look::

    1. LOGI LOGI   Windows 10 x64 Edition  Google Chrome 92.0.4515.107

      @于长野假的@(捂嘴笑)