In this code, I call a function which counts the number of letters in a string, and return a map of runes. To leverage concurrency, I call the function using goroutines:
func ConcurrentFrequency(l []string) FreqMap {
var wg sync.WaitGroup
wg.Add(len(l))
m := FreqMap{}
// Using unbuffered channel
// ch := make(chan FreqMap, len(l))
ch := make(chan FreqMap)
for _, s := range l {
go func(s string, ch chan<- FreqMap) {
defer wg.Done()
ch <- Frequency(s)
}(s, ch)
}
go func() {
wg.Wait()
close(ch)
}()
for cm := range ch {
for r, n := range cm {
m[r] = n
}
}
return m
}
If I try this code without using a waitgroup and the goroutine which closes the channel:
go func() {
wg.Wait()
close(ch)
}()
, then I get a deadlock.
What I don't understand, is why I am able to loop over the unbuffered channel, and read multiple maps from it.
This is the full program: https://go.dev/play/p/zUwr_HvTT5w
And the concurrent method is barely faster than the sequential method:
goos: linux
goarch: amd64
pkg: letter
cpu: Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
BenchmarkSequentialFrequency
BenchmarkSequentialFrequency-2 2820 367128 ns/op 17571 B/op 13 allocs/op
BenchmarkConcurrentFrequency
BenchmarkConcurrentFrequency-2 4237 282632 ns/op 12682 B/op 72 allocs/op
PASS
ok letter 3.320s
CodePudding user response:
A for-range loop over a channel continues until the channel is closed.
If you remove the goroutine that eventually closes the channel, the for loop can never terminate. Once all goroutines sending values are done there is only one goroutine remaining and it is blocked forever, waiting for the channel to be closed.
Buffered channels have nothing to do with this problem. They only help with blocked senders, but here the problem is a blocked receiver.