Killing Me Softly, Or: How Goroutines Want To Exit
If you have worked with POSIX threads in the past, or with threading libraries in various languages, you might have been surprised by the completely different approach to managing concurrency.
Learn debugging with Matt, at 40% off
(and support AppliedGo this way)!
Use the coupon code APPLIEDGO40 at ByteSizeGo.com.
(Affiliate Link)
POSIX threads, or system threads, come with a big dashboard full of control knobs, or rather, a set of control functions such as pthread_atfork(3)
, pthread_attr_init(3)
, pthread_cancel(3)
, pthread_cleanup_push(3)
, pthread_cond_signal(3)
, pthread_cond_wait(3)
, pthread_create(3)
, pthread_detach(3)
, pthread_equal(3)
, pthread_exit(3)
, pthread_key_create(3)
, pthread_kill(3)
, pthread_mutex_lock(3)
, pthread_mutex_unlock(3)
, pthread_mutexattr_destroy(3)
, pthread_mutexattr_init(3)
, pthread_once(3)
, pthread_spin_init(3)
, pthread_spin_lock(3)
, pthread_rwlockattr_setkind_np(3)
, pthread_setcancelstate(3)
, pthread_setcanceltype(3)
, pthread_setspecific(3)
, pthread_sigmask(3)
, pthread_sigqueue(3)
, or pthread_testcancel(3)
.
Goroutines, on the other hand, have only one “knob”:
The go
directive.
No create, cancel, cancel states, detach, fork handling, thread variable creation magic, or kill mechanism.
Especially, no kill mechanism.
Which raises one or another eyebrow along with the question, “but how can I stop a goroutine from the outside?”
How to stop a goroutine from the outside
Short answer: you can't.
Long answer: You can't, and you shouldn't, and you can instead politely ask the goroutine to exit.
You can't, because there's no kill switch.
You shouldn't, as killing concurrently executing code from the outside can have unwanted side effects on open resources or other parts of the program that are in exchange with that concurrent code.
How to ask a goroutine to exit
However, you can politely ask the goroutine to exit, and you even have several easy ways to do that:
- By closing a channel that the goroutine is listening to.
- By sending a value to a channel that the goroutine is listening to.
- By passing a
context.Context
to the goroutine and callingcontext.CancelFunc
from the outside.
Exiting through channel communication
The first two options are suitable for simple scenarios, especially if you have no Context
to pass around.
Here is how to have a goroutine exit by closing a channel:
package main
import (
"fmt"
"time"
)
func main() {
quit := make(chan struct{})
go func() {
for {
select {
case <-quit:
fmt.Println("goroutine exiting")
return
default:
fmt.Println("goroutine working")
time.Sleep(1 * time.Second)
}
}
}()
time.Sleep(3 * time.Second)
fmt.Println("asking to exit")
close(quit)
time.Sleep(1 * time.Second)
}
The case <-quit
case will be selected when the quit
channel is closed, because once a channel is closed, the readers start recieving the zero value of the channel's type immediately.
Note that it's the goroutine's responsibility to exit when the request arrives. Within the <- quit
case, the goroutine can do necessary cleanup such as closing resources or returning them to a resource pool.
The second option is similar, but instead of closing the channel, you send a value to it:
quit <- struct{}{}
(Put this in place of close(quit)
above.)
Exiting through context.Context
The third option is more flexible and is the standard way to exit goroutines that use a Context
.
A quick refresher: context.Context
is an interface that carries deadlines, cancellation signals, and request-scoped values across API boundaries and between processes.
To exit a goroutine using a Context
, you create a WithCancel
context that has a Done
channel and provides a cancel function. By calling the cancel function, you signal the goroutine to exit.
Here's an example:
func main() {
ctx, cancel := context.WithCancel(context.Background())
go func() {
for {
select {
case <-ctx.Done():
fmt.Println("goroutine exiting")
return
default:
fmt.Println("goroutine working")
time.Sleep(1 * time.Second)
}
}
}()
time.Sleep(3 * time.Second)
fmt.Println("cancelling")
cancel()
time.Sleep(1 * time.Second)
}
Goroutines are a simple concurrency mechanism
Goroutines are ideally suited for one-time concurrent work. They are extremely lightweight and have a low starting overhead. There is no need to “remote-control” goroutines to start, stop, pause, or resume them. If the work you want to implement concurrently is so complex that you need such mechanisms, forking or calling a new process is probably an approach worth considering. Or you simplify the concurrent work enough so that you won't need any sophisticated goroutine runtime management.