《GO并发编程实战》—— 临时对象池

声明:本文是《Go并发编程实战》的样章,感谢图灵授权并发编程网站发布样章,禁止以任何形式转载此文。

本章要讲解的是sync.Pool类型。我们可以把sync.Pool类型值看作是存放可被重复使用的值的容器。此类容器是自动伸缩的、高效的,同时也是并发安全的。为了描述方便,我们也会把sync.Pool类型的值称为临时对象池,而把存于其中的值称为对象值。至于为什么要加“临时“这两个字,我们稍后再解释。
我们在用复合字面量初始化一个临时对象池的时候可以为它唯一的公开字段New赋值。该字段的类型是func() interface{},即一个函数类型。可以猜到,被赋给字段New的函数会被临时对象池用来创建对象值。不过,实际上,该函数几乎仅在池中无可用对象值的时候才会被调用。
类型sync.Pool有两个公开的方法。一个是Get,另一个是Put。前者的功能是从池中获取一个interface{}类型的值,而后者的作用则是把一个interface{}类型的值放置于池中。

 通过Get方法获取到的值是任意的。如果一个临时对象池的Put方法未被调用过,且它的New字段也未曾被赋予一个非nil的函数值,那么它的Get方法返回的结果值就一定会是nil。我们稍后会讲到,Get方法返回的不一定就是存在于池中的值。不过,如果这个结果值是池中的,那么在该方法返回它之前就一定会把它从池中删除掉。
这样一个临时对象池在功能上看似与一个通用的缓存池相差无几。但是实际上,临时对象池本身的特性决定了它是一个“个性”非常鲜明的同步工具。我们在这里说明它的两个非常突出的特性。
第一个特性是,临时对象池可以把由其中的对象值产生的存储压力进行分摊。更进一步说,它会专门为每一个与操作它的Goroutine相关联的P都生成一个本地池。在临时对象池的Get方法被调用的时候,它一般会先尝试从与本地P对应的那个本地池中获取一个对象值。如果获取失败,它就会试图从其他P的本地池中偷一个对象值并直接返回给调用方。如果依然未果,那它只能把希望寄托于当前的临时对象池的New字段代表的那个对象值生成函数了。注意,这个对象值生成函数产生的对象值永远不会被放置到池中。它会被直接返回给调用方。另一方面,临时对象池的Put方法会把它的参数值存放到与当前P对应的那个本地池中。每个P的本地池中的绝大多数对象值都是被同一个临时对象池中的所有本地池所共享的。也就是说,它们随时可能会被偷走。
临时对象池的第二个突出特性是对垃圾回收友好。垃圾回收的执行一般会使临时对象池中的对象值被全部移除。也就是说,即使我们永远不会显式的从临时对象池取走某一个对象值,该对象值也不会永远待在临时对象池中。它的生命周期取决于垃圾回收任务下一次的执行时间。
请读者阅读一下这段代码:

package main

import (
    "fmt"
    "runtime"
    "runtime/debug"
    "sync"
    "sync/atomic"
)

func main() {
    // 禁用GC,并保证在main函数执行结束前恢复GC
    defer debug.SetGCPercent(debug.SetGCPercent(-1))
    var count int32
    newFunc := func() interface{} {
        return atomic.AddInt32(&count, 1)
    }
    pool := sync.Pool{New: newFunc}

    // New 字段值的作用
    v1 := pool.Get()
    fmt.Printf("v1: %v\n", v1)

    // 临时对象池的存取
    pool.Put(newFunc())
    pool.Put(newFunc())
    pool.Put(newFunc())
    v2 := pool.Get()
    fmt.Printf("v2: %v\n", v2)

    // 垃圾回收对临时对象池的影响
    debug.SetGCPercent(100)
    runtime.GC()
    v3 := pool.Get()
    fmt.Printf("v3: %v\n", v3)
    pool.New = nil
    v4 := pool.Get()
    fmt.Printf("v4: %v\n", v4)
}

在这里,我们使用runtime/debug代码包的SetGCPercent函数来禁用、恢复GC以及指定垃圾收集比率(详见第7章的第1节中的相关说明),以保证我们的演示能够如愿进行。
我们把这段代码存放在gocp项目的sync1/pool代码包的文件pool_demo.go中,并使用go run命令运行它。就像下面这样:
hc@ubt:~/golang/goc2p/src/sync1/pool$ go run pool_demo.go
而后,我们会在标准输出上看到如下内容:

v1: 1
v2: 2
v3: 5
v4: <nil>

请读者注意第3行和第4行的内容,也就是我们在手动的进行垃圾回收之后的输出内容。在把nil赋给pool的New字段之前,即使手动的执行了垃圾回收,我们也是可以从临时对象池获取到一个对象值的。而在这之后,我们却只能取出nil。读者应该可以依据我们刚刚描述的那两个特性想明白如此输出的原因。
看到这里,读者可能会隐约的感觉到,我们在使用临时对象池的时候应该依照一些方式方法,否则就会很容易迈入陷坑。实际情况确实如此。
首先,我们不能对通过Get方法获取到的对象值有任何假设。到底哪一个值会被取出是完全不确定的。这是因为我们总是不能得知操作临时对象池的Goroutine在哪一时刻会与哪一个P相关联,尤其是在比上述示例更加复杂的程序的运行过程中。在这种情况下,我们也就无从知晓我们放入的对象值会被存放到哪一个本地池中,以及哪一个Goroutine执行的Get方法会返回该对象值。所以,我们给予临时对象池的对象值生成函数所产生的值以及通过调用它的Put方法放入到池中的值都应该是无状态的或者状态一致的。从另一方面说,我们在取出并使用这些值的时候也不应该以其中的任何状态作为先决条件。这一点非常的重要。
第二个需要注意的地方实际上与我们前面讲到的第二个特性紧密相关。临时对象池中的任何对象值都有可能在任何时候被移除掉,并且根本不会通知该池的使用方。这种情况常常会发生在垃圾回收器即将开始回收内存垃圾的时候。如果这时临时对象池中的某个对象值仅被该池引用,那么它还可能会在垃圾回收的时候被回收掉。因此,我们也就不能假设之前放入到临时对象池的某个对象值会一直待在池中,即使我们没有显式的把它从池中取出。甚至一个对象值可以在临时对象池中待多久,我们也无法假设。除非我们像前面的示例那样手动的控制GC的启停。不过,我们并不推荐这种方式。这会带来一些其他问题。
依据我们刚刚讲述的临时对象池特性和使用注意事项,读者应该可以想象得出临时对象池的一些适用场景(比如作为临时且状态无关的数据的暂存处),以及一些不适用的场景(比如用来存放数据库连接的实例)。如果我们在做实现技术的选型的时候把临时对象池作为了候选之一,那么就应该好好想想它的“个性”是不是符合你的需要。如果真的适合,那么它的特性一定会为你的程序增光添彩,无论在功能上还是在性能上。而如果它被用在了不恰当的地方,那么就只能适得其反了。 

时间: 2024-10-29 14:31:09

《GO并发编程实战》—— 临时对象池的相关文章

《Java 7并发编程实战手册》第六章并发集合

由人民邮电出版社出版的<Java 7并发编程实战手册>终于出版了,译者是俞黎敏和申绍勇,该书将于近期上架.之前并发编程网组织翻译过此书,由于邮电出版社在并发网联系他们之前就找到了译者,所以没有采用并发网的译稿,但邮电出版社将于并发网展开合作,发布该书的样章(样章由并发网挑选,你也可以回帖告诉我们你想看哪一章的样章),并组织赠书活动回馈给活跃读者.活动详情请时刻关注并发网的微博和微信(微信号:ifeves),最后祝各位用餐愉快!:) 本章将介绍下列内容: 使用非阻塞式线程安全列表 使用阻塞式线程

《GO并发编程实战》—— 只会执行一次

声明:本文是<Go并发编程实战>的样章,感谢图灵授权并发编程网站发布样章,禁止以任何形式转载此文. 现在,让我们再次聚焦到sync代码包.除了我们介绍过的互斥锁.读写锁和条件变量,该代码包还为我们提供了几个非常有用的API.其中一个比较有特色的就是结构体类型sync.Once和它的Do方法. 与代表锁的结构体类型sync.Mutex和sync.RWMutex一样,sync.Once也是开箱即用的.换句话说,我们仅需对它进行简单的声明即可使用,就像这样: var once sync.Once o

《GO并发编程实战》—— 原子操作

声明:本文是<Go并发编程实战>的样章,感谢图灵授权并发编程网站发布样章,禁止以任何形式转载此文. 我们已经知道,原子操作即是进行过程中不能被中断的操作.也就是说,针对某个值的原子操作在被进行的过程当中,CPU绝不会再去进行其它的针对该值的操作.无论这些其它的操作是否为原子操作都会是这样.为了实现这样的严谨性,原子操作仅会由一个独立的CPU指令代表和完成.只有这样才能够在并发环境下保证原子操作的绝对安全. Go语言提供的原子操作都是非侵入式的.它们由标准库代码包sync/atomic中的众多函

《GO并发编程实战》—— 条件变量

声明:本文是<Go并发编程实战>的样章,感谢图灵授权并发编程网站发布样章,禁止以任何形式转载此文. 我们在第6章讲多线程编程的时候详细说明过条件变量的概念.原理和适用场景.因此,我们在本小节仅对sync代码包中与条件变量相关的API进行简单的介绍,并使用它们来改造我们之前实现的*myDataFile类型的相关方法. 在Go语言中,sync.Cond类型代表了条件变量.与互斥锁和读写锁不同,简单的声明无法创建出一个可用的条件变量.为了得到这样一个条件变量,我们需要用到sync.NewCond函数

《GO并发编程实战》—— 锁的使用

声明:本文是<Go并发编程实战>的样章,感谢图灵授权并发编程网站发布样章,禁止以任何形式转载此文. 在本节,我们对Go语言所提供的与锁有关的API进行说明.这包括了互斥锁和读写锁.我们在第6章描述过互斥锁,但却没有提到过读写锁.这两种锁对于传统的并发程序来说都是非常常用和重要的. 互斥锁 互斥锁是传统的并发程序对共享资源进行访问控制的主要手段.它由标准库代码包sync中的Mutex结构体类型代表.sync.Mutex类型(确切地说,是*sync.Mutex类型)只有两个公开方法--Lock和U

《GO并发编程实战》—— Concurrent Map

声明:本文是<Go并发编程实战>的样章,感谢图灵授权并发编程网站发布样章,禁止以任何形式转载此文. 我们在本章前面的部分中对Go语言提供的各种传统同步工具和方法进行了逐一的介绍.在本节,我们将运用它们来构造一个并发安全的字典(Map)类型. 我们已经知道,Go语言提供的字典类型并不是并发安全的.因此,我们需要使用一些同步方法对它进行扩展.这看起来并不困难.我们只要使用读写锁将针对一个字典类型值的读操作和写操作保护起来就可以了.确实,读写锁应该是我们首先想到的同步工具.不过,我们还不能确定只使用

《GO并发编程实战》—— WaitGroup

声明:本文是<Go并发编程实战>的样章,感谢图灵授权并发编程网站发布样章,禁止以任何形式转载此文. 我们在第6章多次提到过sync.WaitGroup类型和它的方法.sync.WaitGroup类型的值也是开箱即用的.例如,在声明 var wg sync.WaitGroup 之后,我们就可以直接正常使用wg变量了.该类型有三个指针方法,即Add.Done和Wait. 类型sync.WaitGroup是一个结构体类型.在它之中有一个代表计数的字段.当一个sync.WaitGroup类型的变量被声

《C++并发编程实战》——第1章 你好,C++并发世界

第1章 你好,C++并发世界 C++并发编程实战本章主要内容 何谓并发和多线程为什么要在应用程序中使用并发和多线程C++并发支持的发展历程一个简单的C++多线程程序是什么样的这是令C++用户振奋的时刻.距1998年初始的C++标准发布13年后,C++标准委员会给予程序语言和它的支持库一次重大的变革.新的C++标准(也被称为C++11或C++0x)于2011年发布并带来了很多的改变,使得C++的应用更加容易并富有成效. 在C++11标准中一个最重要的新特性就是支持多线程程序.这是C++标准第一次在

《Java并发编程实战》学习笔记 线程安全、共享对象和组合对象

第一章 介绍 线程的优势: 充分利用多处理器 简化模型 简化异步事件的处理 提供用户界面的响应(时间) 线程的风险: 安全的风险(不好的事情会发生),提高错误出现的几率 活性的风险(好的事情不会发生),如某些代码不会执行,出现死锁.活锁以及饥饿 性能的风险,不好的多线程编程可能会危害性能 第二章 线程安全 编写线程安全的代码,实质是管理对状态的访问,尤其是那些共享.可变的状态.对象的状态包括任何能影响它外部可见行为的数据. 当有过个线程能访问状态变量时,而且他们当中能对变量进行修改,则需要对他们