从 0 到 1 手撸一个协程池

33 天前
 IIInsomnia

最近使用 ants ,发现任务不支持 context ,而且非阻塞模式下,拿不到 worker ,会返回 error ;于是决定自己实现一个,非阻塞模式下,任务会缓存到全局链表中待执行,性能还不错 https://github.com/shenghui0779/nightfall

3762 次点击
所在节点    Go 编程语言
45 条回复
ninjashixuan
33 天前
写了好些年 go 没有用过任何协程池的东西,要控制协程数量的地方基本都是自己实现。
yuan1028
33 天前
@kuanat 请教一下大佬,有 go 协程池相关的工程实践文章分享吗?比如使用协程池和不使用协程池,如 cpu 、内存利用率等指标的提升。因为我接触到的也就公司网关的项目利用协程池,普通的服务似乎只需要 k8s 扩容就可以
bv
33 天前
实现协程池需要这么多代码?
kuanat
32 天前
@lesismal #18

我遇到需要极大协程池的只有一个负载平衡路由的 dispatcher ,然而“通用”协程池动态调节对于这个场景也是没有意义的,一次性预热分配好常驻就可以了。

我这里很多资深(职级)程序员都有一种对于 universal/variable/dynamic 方案的迷思,反倒需要我经常强调动态可变实现带来的不可预测性才是大忌。即便是一个理想的动态实现,使用的时候还是会根据硬件、网络实测一个并发上限并一直沿用下去,那这和写一个固定的实现没有什么区别。且不说真正业务产生的 cpu 和内存消耗数量级层面就高于调度器的损耗,即便节省出来资源,也不可能在这种有关键性能的位置搞 cpu 或者内存共享。

也许我这样说有点暴论,但我认为,一个承载能力为 N 的系统和一个承载能力在 0~N 之间变动的系统在生产环境是没有区别的,而前者的复杂度耕地,可维护性更高。
kuanat
32 天前
@yuan1028 #22

我一样很少用到,过去几年里都是如此。你可以看 #24 的回复,我跟你的观点是一致的,动态扩容是实例层面的事情,go 应用单体不应该考虑。这里讨论的是真正存在压力的生产环境。

性能指标这方面,还是要具体情况具体分析。我是极其反对单纯以 cpu 或内存占用作为性能指标的,除非它是真正的瓶颈。笼统地说我一般会跑一下火焰图和计时任务,观察一下可能的热点或者瓶颈在哪里,只有确认了瓶颈存在才会考虑优化。网络编程这个领域,多数时候加钱( cpu 和内存)比加人工更有效。

具体到并发相关的参数,考虑一般有限资源环境中的调度问题,调度行为一定会产生损耗,根据特定的业务场景,追求低延迟就看前 5%的耗时,追求高吞吐就看 95% 的完成时间。看统计分布比看单一指标更有意义。
main1234
32 天前
@bli22ard IM socket 大并发场景
whirlp00l
32 天前
大多数情况下 标准库用起来就够了 https://pkg.go.dev/golang.org/x/sync/errgroup#Group.SetLimit
lesismal
32 天前
@kuanat #24

> 一次性预热分配好常驻就可以了

没必要, runtime 复用协程的性能是足够的, 我们做 RPC 框架的压测, 每个 RPC Call 都是 go func() 一个协程处理, 处理完后就退出了, 每秒几十万 Call, 也就是每秒几十万次 go func(), 毫无压力. 可以参考鸟窝老师的文章, 这里的 RPC 框架的测试都是每个 RPC Call 都 go func() 的, go func() 成本很低的:
https://colobu.com/2022/07/31/2022-rpc-frameworks-benchmarks/

所以, 预热分配除了浪费常驻内存, 额外的 chan 或者 cond_t 反而可能比直接 go func()还慢, 真的没什么必要, 限制协程数量+队列+go func() 可能才是更优解
lesismal
32 天前
@kuanat

> 反倒需要我经常强调动态可变实现带来的不可预测性才是大忌。

接#28, go func()的成本很低, 如果稳定性有问题, 那 runtime 也不稳定了, 预热创建也会不稳定. 较早版本的 go 可能没这么优秀, 1.15 还是 1.18 哪个版本之后来着, runtime go func()和协程复用已经足够优秀了, 所以你不必太担忧这个问题

> 即便是一个理想的动态实现,使用的时候还是会根据硬件、网络实测一个并发上限并一直沿用下去,那这和写一个固定的实现没有什么区别

预热创建和动态创建销毁, 在软硬件的常驻占用和能源消耗硬件持久健康上还是有区别的, 规模越大, 成本效益越大


引起 golang 不稳定的更多是过载, 海量连接之类的导致的协程数量, 对象数量, 对应的内存和调度和 gc 压力, 对应的 oom 和 stw 问题. 为了搞这些, 我搞了这个:
https://github.com/lesismal/nbio
1m websocket connections on 4core cpu ubuntu vm, 1k payload echo test, server costs 1g mem, 能跑 10w qps:
https://github.com/lesismal/go-websocket-benchmark?tab=readme-ov-file#1m-connections-1k-payload-benchmark-for-nbiogreatws
lesismal
32 天前
@kuanat

> 也许我这样说有点暴论,但我认为,一个承载能力为 N 的系统和一个承载能力在 0~N 之间变动的系统在生产环境是没有区别的,而前者的复杂度耕地,可维护性更高。

看上去有道理, 但这就和 javaer 说"堆机器就行了"是一个道理, 都属于看上去对, 但只适用于普通场景

一旦放大到规模效应, 海量的业务, 这差别就大了, 别人努力去搞动态的方向上是对的, 如果实现的方式能达到较优解就更好了
lovelylain
32 天前
协程池就是 c++/java 的惯性思维,go 里面必要性不大,用 chan 控制下协程数量简单又优秀。
weiwenhao
32 天前
协程创建成本很低,就是一个结构体 + 栈空间(如果是无栈协程成本就更低)。 栈空间和结构体所需内存 golang runtime 已经通过 fixalloc + pool 实现了,原则上是不需要协程池了。
fgwmlhdkkkw
32 天前
脱裤子放屁。
hanxiV2EX
32 天前
之前写过一个 go 任务库,也遇到了要不要用协程池的问题,顺便请教一下大家。

目前的实现是一个 service 一个 goroutine ,想改成一个 service 有多个 goroutine ,就是每次发起 call 调用时就新建一个 goroutine ,但是要限制同一个时刻确保一个 service 只有一个 goroutine 在运行,其他的 goroutine 要等别的执行完再执行。不知道如何实现。

仓库地址 https://github.com/hanxi/gtask
hxzhouh1
32 天前
@hanxiV2EX 在加个 chan ?
mu1er
32 天前
赞一个,有空学习学习
lxz6597863
32 天前
hanxiV2EX
32 天前
@hxzhouh1 不知道怎么加。
CloveAndCurrant
32 天前
目前用的这个: https://github.com/sourcegraph/conc
感觉感觉挺不错,使用方式还挺多
IIInsomnia
32 天前
其实我一开始也只是控制协程的数量,执行完 return 交给 runtime 回收,但是在公司项目中压测结果不理想,虽然控制了数量,但是会频繁的创建,导致在公司项目中压测 CPU 和内存不理想,最终还是选择了协程复用;不过我这个不是真正意义上的池化,而是采用「生产-消费」模式

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1089474

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX