仅执行一次
确保在多线程情况下有些任务仅执行一次,类似于单例模式。
如下是 Java 中的单例模式的示例代码。
public class Singleton {
// volatile 修饰,保证多线程下的可见行
private static volatile Singleton INSTANCE=null;
// 构造方法用private 修饰,防止外部实例化
private Singleton(){
}
// 只能通过 Singleton.getIntance() 的方式获取一个实例对象
public static Singleton getIntance(){
// 双检锁
if(INSTANCE==null){
synchronized (Singleton.class){
if(INSTANCE==null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
在 go 语言中可以使用 sync.Once 来创建代理对象。once.Do 指定的方法最多只会被执行一次。
package singleton
import (
"fmt"
"sync"
"testing"
)
type Singleton struct {
name string
}
var singleInstance *Singleton
var once sync.Once
func GetSingletonObj() *Singleton {
// 这个方法只会执行一次,因此就不需要判断是否为空,不需要双检锁这样的机制
once.Do(func() {
fmt.Println("Create Singleton obj.")
// new(Singleton) 返回的是一个地址,因此singleInstance需要声明为指针类型
singleInstance = new(Singleton)
})
return singleInstance
}
func TestSingletonObj(t *testing.T) {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
obj := GetSingletonObj()
// %p用来输出指针的值、输出地址符, %x输出无符号以十六进制表示的整数
t.Logf("obj 对象的地址为 %p", obj)
//t.Logf("obj 对象的地址为 %x", unsafe.Pointer(obj))
wg.Done()
}()
}
wg.Wait()
}
输出结果如下。从输出中可以发现不同的协程中获得的 obj 对象的地址都是同一个,说明返回的是同一个对象。
=== RUN TestSingletonObj
Create Singleton obj.
singleton_test.go:33: obj 对象的地址为 0xc00008a000
singleton_test.go:33: obj 对象的地址为 0xc00008a000
singleton_test.go:33: obj 对象的地址为 0xc00008a000
singleton_test.go:33: obj 对象的地址为 0xc00008a000
singleton_test.go:33: obj 对象的地址为 0xc00008a000
singleton_test.go:33: obj 对象的地址为 0xc00008a000
singleton_test.go:33: obj 对象的地址为 0xc00008a000
singleton_test.go:33: obj 对象的地址为 0xc00008a000
singleton_test.go:33: obj 对象的地址为 0xc00008a000
singleton_test.go:33: obj 对象的地址为 0xc00008a000
--- PASS: TestSingletonObj (0.00s)
PASS
仅需其中一个任务执行完毕
有多个任务,但是我们只需要其中一个任务返回数据即可。比如我们使用聚合搜索,有bing搜索引擎,百度搜素引擎,360搜索引擎,Google搜素引擎,我们查询时,只需要其中一个搜索引擎返回数据即可。
示例代码如下
func runTask(id int) string {
time.Sleep(10 * time.Millisecond)
// fmt.Sprintf 格式化返回一个字符串
return fmt.Sprintf("The result is from %d", id)
}
func FirstResponse() string {
numOfRunner := 10
ch := make(chan string)
for i := 0; i < numOfRunner; i++ {
go func(i int) {
ret := runTask(i)
ch <- ret
}(i)
}
// 一旦chan中有数据,return语句就不会阻塞在这里,就会立即返回
// 也就是说,只要其中一个 Runner 返回了数据,整个程序就立马结束
return <-ch
}
func TestFirstResponse(t *testing.T) {
t.Log(FirstResponse())
}
输出。因为协程的调度机制,每次返回数据都不同,但是有且仅有一条。
=== RUN TestFirstResponse
first_response_test.go:29: The result is from 3
--- PASS: TestFirstResponse (0.01s)
PASS
继续看。通过 runtime.NumGoroutine() 可以得到系统正存在的协程数。
func TestFirstResponse(t *testing.T) {
// runtime.NumGoroutine(),输出当前正存在的协程数
t.Log("Before goroutine num:", runtime.NumGoroutine())
t.Log(FirstResponse())
// 让所有协程都能得到运行
time.Sleep(time.Second * 1)
t.Log("After goroutine num:", runtime.NumGoroutine())
}
输出。最开始时 go routine 为2,这是因为该程序是在测试中运行的,测试程序会在主协程中再启动一个子协程进行运行,所以一开始会存在2个协程。然后其中一个协程最先往chan中发送数据,并陷入阻塞等待,等待接收者接收数据,然后 t.Log(FirstResponse()) 所在的协程从chan中接收到数据,最先发送数据的那个协程从阻塞中退出,执行完毕。但是其它的协程依然在往 chan 中发送数据,但是已经没有接收方再接收数据了,所以它们将陷入等待,因此最后还存在 9 + 2(固有的)= 11个协程。这就造成了资源的浪费,会有大量协程阻塞得不到释放。
=== RUN TestFirstResponse
first_response_test.go:31: Before goroutine num: 2
first_response_test.go:32: The result is from 4
first_response_test.go:34: After goroutine num: 11
--- PASS: TestFirstResponse (1.01s)
PASS
解决方法,将 channel换成带 buffer 的 channel,只需要在使用make创建channel时指定buffer大小即可。
func runTask(id int) string {
time.Sleep(10 * time.Millisecond)
// fmt.Sprintf 格式化返回一个字符串
return fmt.Sprintf("The result is from %d", id)
}
func FirstResponse() string {
numOfRunner := 10
//带buffer的channel
ch := make(chan string, numOfRunner)
for i := 0; i < numOfRunner; i++ {
go func(i int) {
ret := runTask(i)
ch <- ret
}(i)
}
// 一旦chan中有数据,return语句就不会阻塞在这里,就会立即返回
// 也就是说,只要其中一个 Runner 返回了数据,整个程序就立马结束
return <-ch
}
func TestFirstResponse(t *testing.T) {
// runtime.NumGoroutine(),输出当前正存在的协程数
t.Log("Before goroutine num:", runtime.NumGoroutine())
t.Log(FirstResponse())
//sleep 让所有的协程都能运行完毕
time.Sleep(time.Second * 1)
t.Log("After goroutine num:", runtime.NumGoroutine())
}
输出如下。会发现运行完毕后,协程的数量变为了2,达到了预期。原因也很简单,带 buffer 的 channel 将发送方与接收方进行了解耦,发送方只需往 buffer 中发送数据,不需要阻塞等待接收方接收数据。
=== RUN TestFirstResponse
first_response_test.go:32: Before goroutine num: 2
first_response_test.go:33: The result is from 9
first_response_test.go:36: After goroutine num: 2
--- PASS: TestFirstResponse (1.01s)
PASS
需要所有任务都完成
方法一:使用 WaitGroup 可以参考我的这篇文章: go语言学习之并发编程
方法二:
func runTask(id int) string {
time.Sleep(10 * time.Millisecond)
// fmt.Sprintf 格式化返回一个字符串
return fmt.Sprintf("The result is from %d", id)
}
func AllResponse() string {
numOfRunner := 10
//带buffer的channel
ch := make(chan string, numOfRunner)
for i := 0; i < numOfRunner; i++ {
go func(i int) {
ret := runTask(i)
ch <- ret
}(i)
}
finalRet := "\n"
// 依次去chan中取数据,直到取出所有的数据,然后再返回。相当于做了一次聚合
for j := 0; j < numOfRunner; j++ {
finalRet += <-ch + "\n"
}
return finalRet
}
func TestFirstResponse(t *testing.T) {
// runtime.NumGoroutine(),输出当前正存在的协程数
t.Log("Before goroutine num:", runtime.NumGoroutine())
t.Log(AllResponse())
//sleep 让所有的协程都能运行完毕
time.Sleep(time.Second * 1)
t.Log("After goroutine num:", runtime.NumGoroutine())
}
输出。从输出中可以看到所有任务都已经被执行了。
=== RUN TestFirstResponse
all_response_test.go:37: Before goroutine num: 2
all_response_test.go:38:
The result is from 1
The result is from 5
The result is from 7
The result is from 6
The result is from 8
The result is from 9
The result is from 3
The result is from 2
The result is from 0
The result is from 4
all_response_test.go:41: After goroutine num: 2
--- PASS: TestFirstResponse (1.01s)
PASS
Process finished with exit code 0
使用Buffered Channel实现对象池
池化思想在开发中很常见。对于一比较珍贵的资源,为了避免重复创建或者是创建完毕回收不当造成浪费,我们选择使用一个“池”来容纳这些资源,需要使用的时候,从池中取出资源,使用完毕,又归还到“池”中。比如数据库连接池,Java中的线程池。
在 go 语言中,我们可以使用 Buffered Channel 来实现这个 “池“。对于一些不易创建且销毁也比较麻烦的资源,我们就可以使用这个 “池" 来容纳这些资源。但是,我们也需要考虑到,Buffered Channel 为了保证在多协程下的同步,同样引入了锁的机制来保证数据的一致性,使用锁,不可避免也会带来相应的开销。因此,在使用 Buffered Channel 时,我们需要考虑:是使用锁带来的消耗大,还是创建资源,销毁资源带来的消耗大。
type ReusableObj struct {
}
type ObjPool struct {
// 用于缓存可重用对象
bufChan chan *ReusableObj
}
func NewObjPool(numOfObj int) *ObjPool {
objPool := ObjPool{}
objPool.bufChan = make(chan *ReusableObj, numOfObj)
for i := 0; i < numOfObj; i++ {
objPool.bufChan <- &ReusableObj{}
}
return &objPool
}
// 获取对象
func (p *ObjPool) GetObj(timeout time.Duration) (*ReusableObj, error) {
select {
case ret := <-p.bufChan:
return ret, nil
case <-time.After(timeout):// 超时控制
return nil, errors.New("time out")
}
}
// 释放对象,做的工作是将使用完毕的对象再次放入对象池
func (p *ObjPool) ReleaseObj(obj *ReusableObj) error {
select {
case p.bufChan <- obj:
return nil
default:
return errors.New("overflow")
}
}
func TestObjPool(t *testing.T) {
// 创建对象池,大小为10,初始会默认往 pool 中放10个对象
pool := NewObjPool(10)
for i := 0; i < 11; i++ {
if v, err := pool.GetObj(time.Second * 1); err != nil {
t.Error(err)
} else {
t.Logf("v的类型为 %T \n", v)
}
}
t.Log("Done")
}
输出如下,由于我们只往 pool 中放了十个对象,但是我们 get 了 11 次,所以,第 11 次会由于 pool 中的对象已经全部被取走了,等待了一秒以后依然无法取得对象,进而会抛出 error。
=== RUN TestObjPool
obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj
obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj
obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj
obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj
obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj
obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj
obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj
obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj
obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj
obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj
obj_pool_test.go:54: time out
obj_pool_test.go:62: Done
--- FAIL: TestObjPool (1.00s)
FAIL
继续测试,如果我们往一个已经装满了对象的对象池中继续放元素,就会出现 overflow error。
func TestObjPool(t *testing.T) {
// 创建对象池,大小为10,初始会默认往 pool 中放10个对象
pool := NewObjPool(10)
if err := pool.ReleaseObj(&ReusableObj{}); err != nil {
t.Error(err)
}
t.Log("Done")
}
输出如下。
=== RUN TestObjPool
obj_pool_test.go:50: overflow
obj_pool_test.go:62: Done
--- FAIL: TestObjPool (0.00s)
FAIL
如果我们把对象从对象池中获取以后,使用完毕便进行归还,那么不管重复取多少次,对象池中都还是有对象可以取。
func TestObjPool(t *testing.T) {
// 创建对象池,大小为10,初始会默认往 pool 中放10个对象
pool := NewObjPool(10)
for i := 0; i < 11; i++ {
if v, err := pool.GetObj(time.Second * 1); err != nil {
t.Error(err)
} else {
t.Logf("v的类型为 %T \n", v)
if err := pool.ReleaseObj(v); err != nil {
t.Error(err)
}
}
}
t.Log("Done")
}
输出如下
=== RUN TestObjPool
obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj
obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj
obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj
obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj
obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj
obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj
obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj
obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj
obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj
obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj
obj_pool_test.go:56: v的类型为 *obj_pool.ReusableObj
obj_pool_test.go:62: Done
--- PASS: TestObjPool (0.00s)
PASS
Process finished with exit code 0
sync.Pool 对象缓存
go 语言中自带了一个 sync.Pool 的工具类,虽然听名字这是一个 pool,可以用来缓存对象,其实不然,每⼀次GC,都会清除 sync.pool 缓存的对象,不能像 Buffered Channel 一样可以长时间保存对象,由于 GC 的发生是go语言内部进行调度的,无法明确 sync.Pool 中缓存对象的生命周期。
注意:Go 1.13对sync.Pool中的对象回收时机策略做出调整。在1.12版本及以前的版本中,在每轮垃圾回收过程中,每个sync.Pool实例中的所有缓存对象都将被无条件回收掉。从1.13版本开始,如果一个sync.Pool实例在上一轮垃圾回收过程结束之后仍然被使用过,则其中的缓存对象将不会被回收掉。此举对于使用sync.Pool来提升效率的程序来说,将大大减少周期性的因为缓存被清除而造成的瞬时效率下降
使用sync.Pool时,同样需要考虑sync.Pool带来的开销,sync.Pool是协程安全,不可避免就会有锁的开销,需要权衡是对象的创建和 GC 代价大还是锁的开销代价大。而且sync.Pool的生命周期受 GC 影响,不适合于做连接池等。
sync.Pool 对象获取
关于 Processor,可以参照如下 go 语言的协程机制原理图来进行理解。我们知道,在Go语言的并发中,不是以线程为基本的运行单元,而是以协程为基本的运行单元。go 自己实现了一个 Processor,从协程的工作队列中选择一个协程交给系统的线程进行执行。如果这个协程长时间没有被执行完毕,那么 go 语言的 Processor 会选择一个新的系统线程,继续执行协程工作队列中的其它协程。
sync.Pool 对象的放回
示例
func TestSyncPool(t *testing.T) {
pool := &sync.Pool{
New: func() interface{} {
fmt.Println("Create a new Object.")
return 100
},
}
//本次获取时,私有对象不存在,共享池中没有对象,会执行Pool中的New方法,新创建对象并返回
// 断言
v := pool.Get().(int)
fmt.Println(v)
//第一次Put时,私有对象不存在,共享池中也没有对象,则保存为私有对象
pool.Put(3)
//第二次Put时,私有对象已经存在,则将对象放入共享池
pool.Put(4)
v1, _ := pool.Get().(int)
fmt.Println(v1)
//本次获取时,私有对象已经返回,私有对象不存在了,共享池中有对象,会返回共享池中的对象
v2, _ := pool.Get().(int)
fmt.Println(v2)
}
输出如下,符合预期。
=== RUN TestSyncPool
Create a new Object.
100
3
4
--- PASS: TestSyncPool (0.00s)
PASS
go的1.12版本及以前的版本中,在每轮垃圾回收过程中,每个sync.Pool实例中的所有缓存对象都将被无条件回收掉。从1.13版本开始,如果一个sync.Pool实例在上一轮垃圾回收过程结束之后仍然被使用过,则其中的缓存对象将不会被回收掉。
我是用的 go 的版本为 1.16
如下,当本轮进行GC前,由于sync.Pool已经被使用过,因此,进行GC后,不会清除sync.Pool中缓存的对象,再次进行Get时,还是能获取到保存到sync.Pool中的对象(如下示例中获取到的是Processor中的私有对象)。
func TestSyncPoolWithGC(t *testing.T) {
pool := &sync.Pool{
New: func() interface{} {
fmt.Println("Create a new Object.")
return 100
},
}
v := pool.Get().(int)
fmt.Println(v)
pool.Put(3)
// 进行GC
runtime.GC()
v1, _ := pool.Get().(int)
fmt.Println(v1)
}
输出
=== RUN TestSyncPoolWithGC
Create a new Object.
100
3
--- PASS: TestSyncPoolWithGC (0.00s)
PASS
当连续进行两次GC时,输出的结果就不相同了。当连续进行两次GC时,sync.Pool中缓存的对象已经被清除掉了,因此再次获取时,不管是私有对象还是共享池中都不存在对象,Get时将会调用New函数创建一个新的对象并返回。
func TestSyncPoolWithGC(t *testing.T) {
pool := &sync.Pool{
New: func() interface{} {
fmt.Println("Create a new Object.")
return 100
},
}
v := pool.Get().(int)
fmt.Println(v)
pool.Put(3)
runtime.GC()
runtime.GC()
v1, _ := pool.Get().(int)
fmt.Println(v1)
}
输出
=== RUN TestSyncPoolWithGC
Create a new Object.
100
Create a new Object.
100
--- PASS: TestSyncPoolWithGC (0.00s)
PASS
如下,考虑在多协程的情况下使用 sync.Pool 。如下,往sync.Pool中Put了3个数,5个协程取依次去取出一个数据,只能够3个协程取数据,剩余2个协程去取数据时,sync.Pool 中已经没有数据了,因此会执行指定的New函数返回一个新的对象。
func TestSyncPoolInMultiGoroutine(t *testing.T) {
pool := &sync.Pool{
New: func() interface{} {
fmt.Println("Create a new object.")
return 10
},
}
pool.Put(100)
pool.Put(200)
pool.Put(300)
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
fmt.Println(pool.Get())
wg.Done()
}(i)
}
wg.Wait()
}