从 0 到 1 手撸一个协程池

1 天前
 IIInsomnia

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

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

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

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

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

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

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

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

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

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

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

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

仓库地址 https://github.com/hanxi/gtask
hxzhouh1
4 小时 20 分钟前
@hanxiV2EX 在加个 chan ?
mu1er
3 小时 53 分钟前
赞一个,有空学习学习
lxz6597863
3 小时 27 分钟前
hanxiV2EX
1 小时 55 分钟前
@hxzhouh1 不知道怎么加。
CloveAndCurrant
5 分钟前
目前用的这个: https://github.com/sourcegraph/conc
感觉感觉挺不错,使用方式还挺多

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

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

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

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

© 2021 V2EX