在golang中,只需要在函数调用前加上关键字去即可创建一个并发任务单元,而这个新建的任务会被放入队列中,等待调度器安排。相比系统的m级别线程栈,goroutine的自定义栈只有2 kb,这使得我们能够轻易创建上万个并发任务,如此对性能提升不少。但随之而来的有以下几个问题:
如何等待所有goroutine的退出
如何限制创建goroutine的数量(信号量实现)
怎么让goroutine主动退出
探索——如何从外部杀死goroutine
本文记录了笔者就以上几个问题进行探究的过程,文中给出了大部分问题的解决方案,同时也抛出了未解决的问题,期待与各位交流:p
开始之前先定义一个常量const N=100以及一个HeavyWork函数,假定该函数具有极其冗长,复杂度高,难以解耦的特性
func HeavyWork (id int) { rand.Seed (int64 (id)) 间隔:=time.Duration (rand.Intn (3) + 1) * time.Second time . sleep(间隔) fmt。Printf (" HeavyWork % 3 d成本% v \ n”, id,间隔) }
以上定义的内容将在之后的代码中直接使用以缩减篇幅、大部分完整代码可在Github: explore-goroutine中找到
"不通过共享内存进行通信;相反,共享内存通信”——的一大设计哲学《共享内存的通信》
翻译成中文就是,用通信来共享内存数据,而不要通过共享内存数据来进行通信。
中去的了goroutine和渠道提供了一种优雅而独特的结构化并发软件的方法,我们可以利用通道(通道)的特性,来实现当前等待goroutine的操作。但是通道并不是当前这个场景的最佳方案,用它来实现的方式是稍显笨拙的,需要知道确定个数的goroutine,同时稍不注意就极易产生死锁,代码如下:
//代码“说说很简单,给我。” 函数main () { 陈waitChan:=(int, 1) 我:=0;我& lt;N;我+ + { 去func (n int) { HeavyWork (n) waitChan & lt; - 1 }(我) } 问:=0 用于范围waitChan { 问+ + 如果问==N { 打破 } } 关上(waitChan) fmt.Println(“完成”) }
上述代码使用了一个缓存大小为1的通道(频道),创建N个goroutine用于运行HeavyWork,每个任务完成后向waitChan写入一个数据,在收到N个完成信号后退出。
但事实上比较优雅的方式是使用去标准库同步,其中提供了专门的解决方案sync.WaitGroup用于等待一个了goroutine集合的结束
//代码“说说很简单,给我。” 函数main () { wg:=sync.WaitGroup {} 我:=0;我& lt;N;我+ + { wg.Add (1) 去func (n int) { 推迟wg.Done () HeavyWork (n) }(我) } wg.Wait () fmt.Println(“完成”) }
关于同步。WaitGroup的具体使用请参照官方文档(GoDoc)同步。WaitGroup,这里不再赘述
信号量(信号量),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。
其中V操作会增加信号量的数值即释放资源,而p操作会减少它即占用资源
那么非常容易想到的就是利用通道(通道)缓存有限的特性,它允许我们可以自实现一个简单的数量控制,就如同使用信号量一般,在这基础再加上前面提到的sync.WaitGroup,我们可以打出一套组合拳,提供可阻塞的信号量PV操作,能够实现固定创建goroutine数量并且支持等待当前goroutine的退出。结构体定义如下:
型信号量结构{ 线程陈int Wg sync.WaitGroup }
而p操作只需在通道中加入一个元素同时调用WaitGroup.Add即可,这一操作完成对资源的申请
func (sem *信号量)P () { 扫描电镜。线程& lt; - 1 sem.Wg.Add (1) }
相反则是V操作,进行资源的释放
func (sem *信号量)V () { sem.Wg.Done () & lt; -sem.Threads }
等则阻塞等待直到当前所有资源都归还,直接调用WaitGroup的方法即可
func (sem *信号)等(){ sem.Wg.Wait () }
完整代码可以在Github:信号量中查看
利用上面的信号量就可以做的到,在一个时刻的了goroutine数量不会超过信号量值的大小,而某个goroutine退出后将返还占用的信号量,而正在等待的goroutine就可以立即申请,下图形象地展现了运行时的状态