原文地址:来,控制并发的Goroutine数funcmain(){userCount:=math.MaxInt64fori:=0;我<用户数;i++{gofunc(iint){//做一些各种业务逻辑处理fmt.Printf("gofunc:%d\n",i)time.Sleep(time.Second)}(i)}}这里,假设userCount是一个外部参数(不可预测,值可能很大),有人会把它全部扔进循环。想想所有并发的goroutines同时做某件事。我觉得这样效率会更高,对吧!那么,你觉得这里有什么问题吗?噩梦般的开始当然,在某些情况下,这可能是个大问题。因为同时扔进这篇文章的是一个极值。我们可以观察下图中的指标分析,看看情况到底有多“破”。下图是上面代码的执行结果:输出结果...gofunc:5839gofunc:5840gofunc:5841gofunc:5842gofunc:5915gofunc:5524gofunc:5916gofunc:8209gofunc:8264signal:killed如果已经执行自己编写代码,在“输出结果”上会遇到如下问题:输出一定量后系统资源占用率持续上升:控制台不再刷新输出最新值信号量:signal:killedsystemloadCPUsystemloadburstinshorttimeIncreasevirtualmemory虚拟内存占用量在短时间内增加topPIDCOMMAND%CPUTIME#TH#WQ#PORTMEMPURGCMPRSPGRPPPIDSTATEBOOSTS...73414test100.201:59.509/10186801M+0B114G+7340373403running*0[1]总结仔细看监控工具的示意图可以知道我其实是间隔执行了两次,可以看到系统之间的使用率是很大。当进程被杀死后,整体值恢复正常。说到这里,回归正题,不控制goroutine的并发数会怎样?大致如下:CPU使用率波动,内存使用率不断上升。另请参阅CMPRS,它指示进程的压缩数据的字节数。主进程已经达到114G+,主进程崩溃(killed)。简单地说,“死机”的原因就是占用系统资源太多。常见的危害如打开文件数(toomanyfilesopen)、内存占用等对服务器的影响非常大,影响自身和关联的应用程序。极有可能造成不可用或响应慢。此外,启动了多个“失控”的goroutine,导致程序流程混乱。”的问题,大家一起想想办法吧。如下:控制/限制并发运行的goroutine数量改变应用的逻辑写入(避免大量占用系统资源和等待)调整服务硬件配置、最大开启数、内存等阈值控制并发数goroutines接下来,正式开始解决这个问题,希望大家在仔细阅读的时候好好想想,因为这个问题在实际项目中真的是太常见了!问题已经抛出来了,你要做的就是想办法解决这个问题。建议大家自己想想技术方案。继续阅读:-)试试chanfuncmain(){userCount:=10ch:=make(chanbool,2)fori:=0;我<用户数;i++{ch<-truegoRead(ch,i)}//time.Sleep(time.Second)}funcRead(chchanbool,iint){fmt.Printf("gofunc:%d\n",i)<-ch}输出结果:gofunc:1gofunc:2gofunc:3gofunc:4gofunc:5gofunc:6gofunc:7gofunc:8gofunc:0好吧,我们似乎很好地控制了“顺序”2和2执行多个goroutines。然而,问题出现了。你仔细数一下输出结果,只有9个值?这显然是错误的。原因是当主协程结束时,子协程也会被终止。所以剩下的goroutine还没来得及输出值就被送上路了(不信你打开time.sleep看看输出量)trysync...varwg=sync.WaitGroup{}funcmain(){userCount:=10fori:=0;我<用户数;i++{wg.Add(1)goRead(i)}wg.Wait()}funcRead(iint){deferwg.Done()fmt.Printf("gofunc:%d\n",i)}好吧,简单地使用sync.WaitGroup也不起作用。并发goroutine的数量不受控制(意味着无法达到本文要求的目标)。总结简单的使用channel或者sync都有明显的缺陷,是行不通的。看看能不能实现组件组合Trychan+sync...varwg=sync.WaitGroup{}funcmain(){userCount:=10ch:=make(chanbool,2)fori:=0;我<用户数;i++{wg.Add(1)goRead(ch,i)}wg.Wait()}funcRead(chchanbool,iint){延迟wg.Done()ch<-truefmt.Printf("gofunc:%d,time:%d\n",i,time.Now().Unix())time.Sleep(time.Second)<-ch}输出结果:gofunc:9,time:1547911938gofunc:1,时间:1547911938gofunc:6,时间:1547911939gofunc:7,时间:1547911939gofunc:8,时间:1547911940gofunc:0,时间:1547911940gofunc:3,时间:1547911941gofunc:417time19,19time:1547911942gofunc:5,time:1547911942从输出结果来看,确实实现了controlgoroutine在2个数字中执行了我们的“业务逻辑”,当然结果集也是乱七八糟的序列输出方案1:简单Semaphore在确定单纯使用chan+sync的方案可行后,我们将流程逻辑重新封装为gsema,主程序变为:import("fmt""time""github.com/EDDYCJY/gsema")varsema=gsema.NewSemaphore(3)funcmain(){userCount:=10fori:=0;我<用户凑新台币;i++{goRead(i)}sema.Wait()}funcRead(iint){defersema.Done()sema.Add(1)fmt.Printf("gofunc:%d,time:%d\n",i,time.Now().Unix())time.Sleep(time.Second)}分析方案上面代码中,程序执行流程如下:设置允许并发数为3循环10次,each每次启动一个goroutine来执行任务。每个goroutine内部使用sema来控制是否阻塞。根据允许的并发数逐步释放goroutine。最后,任务结束。它看起来像一个人。没有什么严重的问题,但是有一个“大”坑。看第二点“每次启动一个goroutine”这句话。这里有一个问题。提前生成这么多goroutine会不会有什么问题?下面我们一起来分析优劣,如下:优劣适合小批量、低复杂度的使用场景。Accepted(看具体业务场景)实际业务逻辑一直阻塞等待运行(因为并发数有限),基本实际业务逻辑损失的性能大于goroutine本身。goroutine本身很轻,只消耗很少的内存。空间和调度。这种等待响应的情况是可以的。等待任务唤醒Semaphore,操作复杂度低,流程简单,易于控制。不适用于具有数百万或数千万goroutines的大容量、高复杂度的使用场景,浪费了很多调度goroutine和内存空间。恰好如果你的服务器接受不了,Semaphore操作的复杂度会增加,你需要管理更多的状态。总结基于什么样的业务场景,用什么样的解决方案来做事情。有足够的时间让你去追求更好更极致的解决方案(用号三方库也可以)使用哪种方案,我觉得主要根据以上两点思考就可以了。没有对错,只有当前业务场景是否可以接受,预启动的goroutine数量是否为你的系统接受得到解决。因为像本文第一节“问题”这样的案例数量巨大,所以案例很少。它没有那些“特色”。所以,使用这种方案灵活控制goroutine的并发数基本就可以了。隔壁的老王又发现了一个新问题。在“方案一”中,在输入输出一体化的情况下,在普通的业务场景下确实是可以的。但是这个新的业务场景比较特殊,需要控制输入的数量来改变允许并发运行的goroutine数量。仔细想想,需要做以下改动:输入/输出必须分离,这样输入/输出就可以分开控制了。应该可以更改for循环中的goroutines并发数(可以设置值)。但它也必须有一个最大值(因为允许更改是相对的)选项二:flexiblechan+syncpackagemainimport("fmt""sync""time")varwgsync.WaitGroupfuncmain(){userCount:=10ch:=使(chanint,5)为我:=0;我<用户数;i++{wg.Add(1)gofunc(){deferwg.Done()ford:=rangech{fmt.Printf("gofunc:%d,time:%d\n",d,time.Now().Unix())time.Sleep(time.Second*time.Duration(d))}}()}fori:=0;我<10;i++{ch<-1ch<-2//time.Sleep(time.Second)}close(ch)wg.Wait()}输出结果:...gofunc:1,time:1547950567gofunc:3,time:1547950567gofunc:1,时间:1547950567gofunc:2,时间:1547950567gofunc:2,时间:1547950567gofunc:3,时间:1547950567gofunc:1,时间:1547950568gofunc:2,时间:5:15371547950568gofunc:1,时间:1547950568gofunc:3,time:1547950569gofunc:2,time:1547950569在“方案二”中,我们可以根据新的业务需求随时随地做如下事情:改变频道的输入数量,根据特殊情况改变频道环境loop值更改允许的最大并发goroutine数。一般来说,可控的空间是尽可能的释放出来。它更灵活吗?:-)方案三:第三方库go-playground/poolnozzle/throttlerJeffail/tunnypanjf2000/蚂蚁也有不少成熟的第三方库。它们基本上是旨在生成和管理goroutines的池工具。我简单列举了一些。具体我建议大家阅读源码或者搜索更多。本文开头总结了类似的原理。我花了很大的力气(极数)告诉大家,并发goroutine的数量会导致系统占用更多的资源。服务最终崩溃的极端情况。为了希望大家以后避免此类问题,给大家留下深刻的印象,我们以“控制goroutine并发数”为主题进行一些分析。分别给出了三种方案。在我看来,每个都有其优点和缺点。我建议大家选择适合自己场景的技术方案,因为解决这个问题的技术方案种类繁多,面孔各异的人千千万万。本文推荐更多常用的解决方案,欢迎大家在评论区继续补充:-)
