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

怎么让goroutine主动退出
对于goroutine的主动退出,比较友好的做法就是循环监听一个channel,通过类似信号的方式来告知goroutine的”该退出了“,然后goroutine自己主动退出,这种做法在网上十分常见,也是Golang官方推荐的做法,思想也很简单。
func main() {
ok, quit := make(chan int, 1), make(chan int, 1)
go func() {
i := 0
for {
select {
case <-quit:
ok <- 1
return
default:
HeavyWork(i)
i++
}
}
}()
time.Sleep(5 * time.Second)
quit <- 1
<-ok
}
运行结果如下图

探索——如何从外部杀死goroutine
上面讲了一些关于goroutines和channel的简单使用,接下来终于写到本文的重点了。笔者并没有解决如何从外部杀死一个goroutine,但记录了尝试“杀死”中的可行或不可行方法,希望对各位有所帮助。
因为近期在开发中遇到这样一个问题,当一个函数是极其冗长、复杂度高、难以解耦的顺序结构代码时(例如某个极其复杂无循环结构的加密算法),而且由于数据量巨大,需要反复调用该函数,由于每运行一次,程序都会消耗大量的时间、空间,那么当一个任务已经被用户抛弃时,如何才能抛弃仍在做着无用功的goroutine?
为了达到“杀死goroutine”的目的,笔者做了很多尝试,如
- select结构(条件实现)
- panic退出机制(失败)
- 获取pid杀死(失败)
- ptrace单步调试(失败)
- ...(失败)
利用select语句实现
关于“如何杀死goroutine”,网上有一部分答案就是利用select实现的,但是这种方式实现的代码并不适用于服务类的程序,但是对于一般非服务类程序的确能够实现杀死goroutine的效果,代码如下:
func main() {
wrapper := func() chan int {
c := make(chan int)
go func() {
HeavyWork(0)
c <- 1
}()
return c
}
select {
case <-wrapper():
case <-time.After(1 * time.Second):
fmt.Println("time limit exceed")
}
// time.Sleep(3 * time.Second)
}

但是一旦主函数没有立即退出,而是作为某种服务而继续运行时,这里删除了main函数的最后一行注释time.Sleep(3 * time.Second),延迟三秒后退出。可以看见尽管已经超时并输出"time limit exceed"之后,HeavyWork在main函数没退出前依旧在运行。效果如下

所以使用select-timeout的方式比较适合实时退出类型的程序,能够实现一定程度上的并发控制,
小结
就目前而言,还没有完美的方案来解决控制goroutine的问题,事实上Go似乎并不允许和推荐人们直接控制goroutine,所以暂时还无法做到从外部直接控制goroutine的生命周期,所以比较推荐的做法还是只能通过goroutine主动退出的方法,循环监听channel,在发出退出信号后最多只消耗一轮资源后就退出,但这就要求该代码具有循环结构否则就很难使用。有更好解决方案的朋友,请务必告诉我!
转载请注明出处:http://www.cnblogs.com/tr3e/p/7995689.html
Golang 探索对Goroutine的控制方法的更多相关文章
- Golang之chan/goroutine(转)
原文地址:http://tchen.me/posts/2014-01-27-golang-chatroom.html?utm_source=tuicool&utm_medium=referra ...
- golang并发编程goroutine+channel(一)
go语言的设计初衷除了在不影响程序性能的情况下减少复杂度,另一个目的是在当今互联网大量运算下,如何让程序的并发性能和代码可读性达到极致.go语言的并发关键词 "go" go dos ...
- Golang 入门 : 等待 goroutine 完成任务
Goroutine 是 Golang 中非常有用的功能,但是在使用中我们经常碰到下面的场景:如果希望等待当前的 goroutine 执行完成,然后再接着往下执行,该怎么办?本文尝试介绍这类问题的解决方 ...
- golang中的goroutine
1. 概念 go中可以并发执行的活动单元称为goroutine当一个go程序启动时,一个执行main function的goroutine会被创建,称为main goroutinego func() ...
- golang学习笔记 --- goroutine
package main import ( "fmt" "io" "io/ioutil" "net/http" &quo ...
- Golang教程:goroutine信道
在上一篇教程中,我们讨论了如何使用协程实现并发.在这篇教程中,我们将讨论信道以及如何使用信道实现协程间通信. 什么是信道 信道(Channel)可以被认为是协程之间通信的管道.与水流从管道的一端流向另 ...
- Golang教程:goroutine协程
在上一篇中,我们讨论了并发,以及并发和并行的区别.在这篇教程中我们将讨论在Go中如何通过Go协程实现并发. 什么是协程 Go协程(Goroutine)是与其他函数或方法同时运行的函数或方法.可以认为G ...
- Golang并发编程——goroutine、channel、sync
并发与并行 并发和并行是有区别的,并发不等于并行. 并发 两个或多个事件在同一时间不同时间间隔发生.对应在Go中,就是指多个 goroutine 在单个CPU上的交替运行. 并行 两个或者多个事件在同 ...
- [golang学习] goroutine调度
这两天有些闲功夫, 学习下golang, 确实非常简洁. 不过有些缺憾. 在我的测试中. golang的调度(goroutine)似乎不是非常好. func say(k int) { fmt.Prin ...
随机推荐
- python爬虫如何入门
学爬虫是循序渐进的过程,作为零基础小白,大体上可分为三个阶段,第一阶段是入门,掌握必备的基础知识,第二阶段是模仿,跟着别人的爬虫代码学,弄懂每一行代码,第三阶段是自己动手,这个阶段你开始有自己的解题思 ...
- linux学习(五)系统目录结构,ls命令,文件类型,alias
一.系统目录结构 在我们的根目录下,有这样一些文件夹 /bin /sbin /usr/bin /usr/sbin /sbin一般都是root用户用的 /boot 系统启动相关的,grup就放在这里,这 ...
- 2017 多校训练 1006 Function
Function Time Limit: 4000/2000 MS (Java/Others) Memory Limit: 131072/131072 K (Java/Others)Total ...
- 0_Simple__simpleAtomicIntrinsics + 0_Simple__simpleAtomicIntrinsics_nvrtc
原子操作.并且在静态代码和运行时编译两种条件下使用. ▶ 源代码:静态使用 #ifndef _SIMPLEATOMICS_KERNEL_H_ #define _SIMPLEATOMICS_KERNEL ...
- 「Vue」起步 - vue-router路由与页面间导航
vue-router 我们知道路由定义了一系列访问的地址规则,路由引擎根据这些规则匹配找到对应的处理页面,然后将请求转发给页进行处理.可以说所有的后端开发都是这样做的,而前端路由是不存在"请 ...
- spa(单页应用)中,使用history模式时,微信长按识别二维码在ios下失效的问题
spa(单页应用,vue)中,使用history模式时,微信长按识别二维码在ios下失效的问题. 触发条件: spa单页应用: 路由模式 history 从其他页面跳转到带有微信二维码识别的页面(不是 ...
- 用UiPath导入RPA实践1:VirtualBox的安装
之前说了关于RPA的一些概念,但对于RPA到底是个什么东西,能干什么,咋么干都没有具体的概念,所以后面分几回,用[UiPath]这个工具为基础说说RPA的实际应用,希望能抛砖引玉. 在试用新的东西 ...
- npm常用命令及版本号浅析
npm 包管理器的常用命令 测试环境为node>=8.1.3&&npm>=5.0.3 1, 首先是安装命令 //全局安装 npm install 模块名 -g //本地安装 ...
- 项目实战2—LVS负载均衡
负载均衡集群企业级应用实战-LVS 实现基于LVS负载均衡集群的电商网站架构 随着业务的发展,网站的访问量越来越大,网站访问量已经从原来的1000QPS,变为3000QPS,网站已经不堪重负,响应缓慢 ...
- JAVA_读取XML文件
在项目中,很多种情况都需要将一些配置写在xml文件或者properties文件里,便于日后修改配置,好维护等等. 1.新建xml文件 <?xml version="1.0" ...