openlang

已取消 context 但出错后 worker goroutine 仍然存活(总是 workers + 2)

大家好,

我在写一个小型 Go CLI,叫 shotcheck,用来检查文件夹里每张 PNG 是否符合 App Store 对应位置要求的精确尺寸。一个 worker 池(默认 8 个)从 jobs 里读文件名,检查后把 Result 发到无缓冲的 results channel。main 里的 collect 负责读取,遇到第一个不合格的就调用 cancel() 并返回错误。表面行为是对的,会打印 checked 11 of 40,然后输出 FAIL 和文件名,退出码为 1。

问题出在 -debug 参数上。失败之后我先短暂 sleep,再打印 runtime.NumGoroutine(),本以为是 1。结果 8 个 worker 时是 10,-workers 4 时是 6,也就是永远是 worker 数 + 2。sleep 时间加长(先 2 秒,再 5 秒)也没有变化。这是我凭上次运行的印象写的,因为我现在这台机器上跑不了 Go。

生产者 goroutine 确实停了,jobs 也关闭了,所以我不明白为什么 context 取消后,其余的 goroutine 没有停。我试过挪动 cancel() 的位置,没变化。我怀疑是 WaitGroup 的问题,或者 defer cancel() 执行得太晚。

在 CLI 里这无伤大雅,因为进程会退出。但我想把这个池放进一个长期运行的 CI 辅助程序里,那样的话我觉得每次失败的运行都会堆积这些 goroutine,把机器拖得很慢。

worker 部分的代码在下面。

for i := 0; i < n; i++ {
	wg.Add(1)
	go func() {
		defer wg.Done()
		for path := range jobs {
			r := checkShot(path)
			results <- r
		}
	}()
}

go func() {
	wg.Wait()
	close(results)
}()

会不会是我漏掉了什么很简单的东西?感谢阅读))

从英语翻译查看原文

后续讨论 4

顺着彼此的依据读下去
  1. 固定链接

    谢谢 fenwick40,我觉得你说得对。collect 返回之后没人再读 results,而这个 channel 又是无缓冲的,所以每个已经拿到结果的 worker 都会永远卡在 results <- r 上。WaitGroup 永远到不了零,close(results) 也就永远不会执行。这就解释了“+ 2”,一个是卡在 wg.Wait() 上等待的 goroutine,可能还有一个辅助 goroutine。cancel 没起作用,是因为 worker 卡在发送上的时候根本不看 ctx。

    所以修复办法是让发送操作遵守 context,如下所示。

    select {
    case results <- r:
    case <-ctx.Done():
        return
    }

    另外在循环里调用 checkShot 之前也要检查 ctx.Done()。我现在跑不了,明天试一下再回来反馈。

    关于“把机器拖得很慢”,可能是我说得夸张了,挂起的 goroutine 开销很小,但在 CI 辅助程序里每次失败的运行还是会泄漏,所以值得修))

    从英语翻译查看原文

同一个问题的其他角度

  1. 把过滤函数标成 async,卡顿一点没变

    ledgerscan 在 400k 条记录上做搜索。我把 run 改成了 async,在 main 里 await 它,本以为 100 ms 的心跳 Timer 会一直跳。结果先是几下 tick,然后打出 searching "rent"...,接着…

    回复 2最近活动 dart
  2. isolate 都结束了,对 ReceivePort 的 await for 却一直不结束

    大家好,我写了个小的 dart 工具,用来汇总温度 CSV,每个文件起一个 isolate,这样大文件不用互相等。我在 WSL 里这样跑 dart run bin/summarize.dart data/north.csv data/south.cs…

    回复 2最近活动 dart
  3. outliers.log 的行数比 :outliers 计数多,8 个 future

    我用 8 个 future 把 20000 条模拟潮汐读数折叠进同一个 atom。每条超过阈值的读数还会被追加到 outliers.log。汇总结果没问题,:n 始终是 20000,:outliers 每次运行都一样,因为种子是固定的。但日志的行数比…

    回复 1最近活动 clojure