안녕하세요,
작은 Go CLI인 shotcheck를 만들고 있어요. 폴더 안의 모든 PNG가 App Store 슬롯에서 요구하는 정확한 크기인지 확인하는 도구예요. worker 풀(기본 8개)이 jobs에서 이름을 읽어서 검사하고, Result를 버퍼 없는 results 채널로 보내요. main의 collect가 이걸 읽다가 처음으로 잘못된 게 나오면 cancel()을 호출하고 에러를 반환해요. 눈에 보이는 동작은 맞아요. checked 11 of 40을 출력하고, 파일 이름과 함께 FAIL, 종료 코드 1이 나와요.
문제는 -debug 플래그예요. 실패한 뒤에 잠깐 sleep하고 runtime.NumGoroutine()을 출력하는데, 1이 나올 거라고 예상했어요. 그런데 worker가 8개면 10이 나오고, -workers 4면 6이 나와요. 그러니까 항상 workers + 2예요. sleep을 더 길게(2초, 그다음 5초) 해봐도 달라지지 않아요. 지금은 이 컴퓨터에서 Go를 못 돌려서, 마지막으로 실행했던 기억으로 쓰고 있어요.
producer goroutine은 멈추고 jobs도 닫히는데, 취소된 context가 왜 나머지를 멈추지 못했는지 모르겠어요. 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)
}()
제가 뭔가 간단한 걸 놓치고 있는 걸까요? 읽어 주셔서 감사해요))
영어에서 번역·원문 보기Hi all,
I am building a small Go CLI, shotcheck, that checks every PNG in a folder against the exact size the App Store slot wants. A pool of workers (8 by default) reads names from jobs, checks them and sends a Result into an unbuffered results channel. collect in main reads them, and on the first bad one calls cancel() and returns an error. Visible behaviour is right: it prints checked 11 of 40, then FAIL with the file name, exit code 1.
My problem is the -debug flag. After a failure I print runtime.NumGoroutine() after a short sleep and expected 1. I get 10 with 8 workers, and 6 with -workers 4, so always workers + 2. A longer sleep (2s, then 5s) changes nothing. I am writing this from memory of my last run, as I can't run Go on this machine right now.
The producer goroutine does stop and jobs is closed, so I don't understand why the canceled context did not stop the rest. I tried moving cancel() around, no change. I suspect the WaitGroup, or defer cancel() running too late.
In the CLI it is harmless because the process exits. But I want this pool inside a long-running CI helper, and there I think these goroutines would pile up on every failed run and slow the machine a lot.
The worker part:
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)
}()
Is it possible that I am missing something simple here? Thanks for reading))