Stopping Goroutines

Mat Ryer
4 min readMay 4, 2015

Goroutines are extremely powerful and must be the simplest way to write concurrent code in any programming language. But getting long-running tasks to stop requires a little magic.

To run a function in the background, it takes three keypresses; g, o and space.

This is a normal function call:

DoSomething()

This is one that does something in the background:

go DoSomething()

Sexy eh?

Another great feature of Go is Channels, which can be used to communicate between goroutines. And they provide the answer for how we might tell a background task that it’s time to pack it in, and go home.

Stop channels

Just by using Go’s in-built channels, you can signal to a background task that it’s time to stop.

We create two channels, a `stopchan` and a `stoppedchan`, to indicate that it’s time to stop and get notified when the stopping (and any teardown) has completed.

// a channel to tell it to stop
stopchan := make(chan struct{})
// a channel to signal that it's stopped
stoppedchan := make(chan struct{})
go func(){ // work in background // close the stoppedchan when this func
// exits
defer close(stoppedchan)
// TODO: do setup work
defer func(){
// TODO: do teardown work
}()
for {
select {
default:
// TODO: do a bit of the work case <-stopchan:
// stop
return
}
}
}()

Here we use the `go` keyword to run a function in the background. It starts an infinite for-loop, which we break only when the `<-stopchan` case occurs in our `select` block. Meanwhile, in the `default` case, we do the work.

Rather than send anything down the `stopchan`, we can just close it to trigger the case:

log.Println("stopping...")close(stopchan)  // tell it to stop
<-stoppedchan // wait for it to have stopped
log.Println("Stopped.")

When the `stopchan` channel is closed, execution will break the `workloop`, where it will do any teardown work before exiting and closing the `stoppedchan` channel that we deferred. Our little code snippet above is blocking on `stoppedchan` waiting for the background task to finish.

This is a very nice solution because it requires no external pacakges and uses pure Go code. You could also give the same `stopchan` to many background routines built using the same technique, which will indicate that you want them all to stop at the same time. Although you would need a `stoppedchan` for each one — or an alternative way to indicate that each routine has stopped.

Introducing the Runner package

The github.com/matryer/runner package, hides the channel complexities behind a very simple interface while also making very clear the intent of the code.

Channels are extremely powerful, which means that sometimes it’s hard to see exactly what they’re being used for. Smart naming and some simple abstractions can make code much easier to read, even at the expense of adding a dependency.

Calling `runner.Go` with a callback function runs the code in a new goroutine, but returns a `task` object which lets you stop the task from the outside.

task := runner.Go(func(shouldStop runner.S) error {
// do setup work
defer func(){
// do tear-down work
}()
for {
// do stuff // periodically check to see if we should
// stop or not.
if shouldStop() {
break
}
}
return nil // no errors
})

Inside the function, create an infinite for loop that gets broken either when the job is finished, or else if the `shouldStop` function returns `true`.

If the task is processing items in a database, then check `shouldStop` after each batch. If the task is making HTTP requests, perhaps it is sensible to check `shouldStop` after each request. The runner package expects that your task will take a little time to finish what it was doing before stopping, so that it may gracefully shut-down without leaving things in an unpredictable state.

Notice that we do not deal with channels at all here. Only when we want to wait for the task to have stopped do we get exposed to channels.

Stopping the task

To stop the task, call `task.Stop()` and then wait for it to stop using the `StopChan()` provided by the `Task`:

task.Stop()
select {
case <-task.StopChan():
// task successfully stopped
case <-time.After(10 * time.Second):
// task didn't stop in time
}
// execution continues once the code has stopped or has
// timed out.
// check task.Err() to see what it returned
if task.Err() != nil {
log.Fatalln("task failed:", task.Err())
}

You may use `task.Err()` to see what the function returned which should be `nil` if everything was OK, or else it will be an `error`.

The `StopChan()` channel is closed when the task has been stopped, which causes the first select case to trigger. `<-time.After` provides an alternative trigger after ten seconds, which should give the task an appropriate amount of time to stop what it was doing before we carry on.

Go Programming Blueprints #shamelessplug

Learn more about the practicalities of Go with my book Go Programming Blueprints.

--

--

Mat Ryer

Founder at MachineBox.io — Gopher, developer, speaker, author — BitBar app https://getbitbar.com — Author of Go Programming Blueprints