bigywater.tech
Go · 82

worker pool:固定并发数的几种写法对比

限制并发数有好几种写法:固定 worker 读 channel、信号量、errgroup.SetLimit。它们都能把同时在做事的任务数限住,但 goroutine 数、内存和速度不一样。这篇用真实运行把它们并排比较,看任务慢和任务极短两种情形。

52 vs 2002
上限 50 的 2000 个慢任务:固定 worker 的峰值 goroutine 数 vs 先起 goroutine 再在里面占位的写法
414 vs 521–649 ms
100 万个极短任务、上限 8:固定 worker vs 每任务一个 goroutine 的限并发写法
27 KiB vs 31 MB
同样这 100 万个极短任务的累计分配

慢任务:并发被限住,goroutine 数不一定

实测 · 本机执行输出
  列:用时 ms / 峰值 goroutine 数 / 同时在做事的最大个数 / 累计分配 KiB / 进程向系统申请的内存 MiB
  固定 N 个 worker 读任务 channel    200 ms /      52 /     50 /        73 KiB /    9 MiB
  信号量:先占位再起 goroutine         201 ms /      52 /     50 /       378 KiB /   13 MiB
  信号量:先起 goroutine 再在里面占位     201 ms /    2002 /     50 /      1475 KiB /   17 MiB
  errgroup.SetLimit(N)        201 ms /      52 /     50 /       398 KiB /   13 MiB
  不限制,每个任务一个 goroutine          6 ms /    2002 /   2000 /      1315 KiB /   17 MiB

五种写法同时在做事的最大个数都符合预期(50;不限制是 2000)。但“先起 goroutine 再在里面抢信号量”会同时存在 2002 个 goroutine,虽然并发受限,内存多申请了几 MiB。先占位再起,或者固定 worker,峰值只有 52。

任务极短时,每任务一个 goroutine 就贵了;怎么选

实测 · 本机执行输出
  列:用时 ms / 峰值 goroutine 数 / 同时在做事的最大个数 / 累计分配 KiB / 进程向系统申请的内存 MiB
  固定 N 个 worker 读任务 channel    414 ms /      10 /      4 /        27 KiB /   13 MiB
  信号量:先占位再起 goroutine         649 ms /     126 /      4 /     31475 KiB /   17 MiB
  信号量:先起 goroutine 再在里面占位     521 ms /     841 /      4 /     31864 KiB /   17 MiB
  errgroup.SetLimit(N)        646 ms /     145 /      4 /     39279 KiB /   17 MiB
  不限制,每个任务一个 goroutine        417 ms /     170 /      6 /     23727 KiB /   21 MiB

100 万个 50 次加法的任务,固定 8 个 worker 用 414 ms、几乎不分配(27 KiB);每任务起一个 goroutine 的写法用 521–649 ms、累计分配 31–39 MB。不限制的写法 417 ms 看起来不慢,但同时有 170 个 goroutine,内存申请 21 MiB,而且没有限制,不能用在任务重的场景。注意这里“同时在做事的最大个数”只有 4–6,因为任务太短几乎不重叠。选择上:任务多且短,用固定 worker 读 channel;任务慢、数量少,errgroup.SetLimit 写起来最省事,性能与手写信号量一样;无论哪种,都别在 goroutine 里面再抢信号量。每种只测了 3 次、任务内容只有睡眠和极短加法两种,结果对真实业务不一定适用。

分步演示

worker pool
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。五种写法:固定 N 个 worker 读 channel、信号量先占位再起 goroutine、先起 goroutine 再在里面占位、errgroup.SetLimit(来自 golang.org/x/sync v0.24.0)、不限制;峰值 goroutine 数由另一个 goroutine 每 200 µs 采样 runtime.NumGoroutine;“同时在做事的最大个数”用原子计数器记录;累计分配 = MemStats.TotalAlloc 差;内存 = MemStats.Sys;每种 3 次取用时中位数那一次
    • 没有验证:任务有返回值或错误时的收集写法、动态调整并发数、优先级队列、真实 CPU 重任务下的差别、第三方 worker pool 库(如 ants)的表现、不同 GOMAXPROCS 下的结果
    京ICP备2026067502号-1京公网安备11010502063487号