Reputation: 2868
Like here I created a go playground sample: sGgxEh40ev, but cannot get it work.
quit := make(chan bool)
res := make(chan int)
go func() {
idx := 0
for {
select {
case <-quit:
fmt.Println("Detected quit signal!")
return
default:
fmt.Println("goroutine is doing stuff..")
res <- idx
idx++
}
}
}()
for r := range res {
if r == 6 {
quit <- true
}
fmt.Println("I received: ", r)
}
Output:
goroutine is doing stuff..
goroutine is doing stuff..
I received: 0
I received: 1
goroutine is doing stuff..
goroutine is doing stuff..
I received: 2
I received: 3
goroutine is doing stuff..
goroutine is doing stuff..
I received: 4
I received: 5
goroutine is doing stuff..
goroutine is doing stuff..
fatal error: all goroutines are asleep - deadlock!
Is this possible? Where am I wrong
Upvotes: 4
Views: 3281
Reputation: 2868
As @icza's answer is pretty clean, which @Ravi's goes to the synchronised way.
But coz I don't want to spend that much effort to restructure the code, and also I don't want to go to the synchronised way, so eventually went to the defer panic recover
flow control, as below:
func test(ch chan<- int, data []byte) {
defer func() {
recover()
}()
defer close(ch)
// do your logic as normal ...
// send back your res as normal `ch <- res`
}
// Then in the caller goroutine
ch := make(chan int)
data := []byte{1, 2, 3}
go test(ch, data)
for res := range ch {
// When you want to terminate the test goroutine:
// deliberately close the channel
//
// `go -race` will report potential race condition, but it is fine
//
// then test goroutine will be panic due to try sending on the closed channel,
// then recover, then quit, perfect :)
close(ch)
break
}
any potential risk with this approach?
Upvotes: 0
Reputation: 417412
The problem is that in the goroutine you use a select
to check if it should abort, but you use the default
branch to do the work otherwise.
The default
branch is executed if no communications (listed in case
branches) can proceed. So in each iteration quit
channel is checked, but if it cannot be received from (no need to quit yet), default
branch is executed, which unconditionally tries to send a value on res
. Now if the main goroutine is not ready to receive from it, this will be a deadlock. And this is exactly what happens when the sent value is 6
, because then the main goroutine tries to send a value on quit
, but if the worker goroutine is in the default
branch trying to send on res
, then both goroutines try to send a value, and none is trying to receive! Both channels are unbuffered, so this is a deadlock.
In the worker goroutine you must send the value on res
using a proper case
branch, and not in the default
branch:
select {
case <-quit:
fmt.Println("Detected quit signal!")
return
case res <- idx:
fmt.Println("goroutine is doing stuff..")
idx++
}
And in the main goroutine you must break out from the for
loop so the main goroutine can end and so the program can end as well:
if r == 6 {
quit <- true
break
}
Output this time (try it on the Go Playground):
goroutine is doing stuff..
I received: 0
I received: 1
goroutine is doing stuff..
goroutine is doing stuff..
I received: 2
I received: 3
goroutine is doing stuff..
goroutine is doing stuff..
I received: 4
I received: 5
goroutine is doing stuff..
goroutine is doing stuff..
Upvotes: 7
Reputation: 1782
The fundamental issue is that producer must always check in between sending values if the consumer (main in your case) has decided to quit reading (in your code this is optional). What's happening is even before the value of quit is sent (and received), the producer goes ahead and sends the next value on res
which the consumer never is able to read - the consumer is in fact trying to send the value on the quit channel expecting the producer to read. Added a debug statement which can help you understand : https://play.golang.org/p/mP_4VYrkZZ, - producer is trying to send 7 on res and blocking, and then consumer trying to send value on quit and blocking. Deadlock!
One possible solution is as follows (using a Waitgroup is optional, needed only if you need a clean exit from producer side before return):
package main
import (
"fmt"
"sync"
)
func main() {
//WaitGroup is needed only if need a clean exit for producer
//that is the producer should have exited before consumer (main)
//exits - the code works even without the WaitGroup
var wg sync.WaitGroup
quit := make(chan bool)
res := make(chan int)
go func() {
idx := 0
for {
fmt.Println("goroutine is doing stuff..", idx)
res <- idx
idx++
if <-quit {
fmt.Println("Producer quitting..")
wg.Done()
return
}
//select {
//case <-quit:
//fmt.Println("Detected quit signal!")
//time.Sleep(1000 * time.Millisecond)
// return
//default:
//fmt.Println("goroutine is doing stuff..", idx)
//res <- idx
//idx++
//}
}
}()
wg.Add(1)
for r := range res {
if r == 6 {
fmt.Println("Consumer exit condition met: ", r)
quit <- true
break
}
quit <- false
fmt.Println("I received: ", r)
}
wg.Wait()
}
Output:
goroutine is doing stuff.. 0
I received: 0
goroutine is doing stuff.. 1
I received: 1
goroutine is doing stuff.. 2
I received: 2
goroutine is doing stuff.. 3
I received: 3
goroutine is doing stuff.. 4
I received: 4
goroutine is doing stuff.. 5
I received: 5
goroutine is doing stuff.. 6
Consumer exit condition met: 6
Producer quitting..
On playground : https://play.golang.org/p/N8WSPvnqqM
Upvotes: 0