Rabarar's Blog

A blogging framework for mild hackers.

GoRoutines, Channels, and Proper Exits

| Comments

I read a great article summarizing the interrelationship between go routines, channel communications, unix signals and proper termination by Adam Presley. In his article he describes how to coordinate starting a go routine, handling an interrupt, and then communicating through a channel to the go routine that the program wants to terminate.

So what do you do if you have more than one go routine? You need to communicate to all and wait for all when you quit. Here’s a contrived example that demonstrates one way to do it.

One point to note. In this example we don’t distinguish between which routines we wish to quit in any particular order. In fact, as implemented here, there is no deterministic way of knowing the order (How might you implement the code so that you would be able to deterministically know the order of go routine termination?)

Here’s an example that demonstrates how you might handle an arbitrary number of go routines:

Let’s say we start off with a constant number of routines we wish to create:

1
2
3
const (
        maxGoRoutines = 50
)

We will start a go routine maxGoRoutines times and then we will ensure that we wait for the same number of routines to complete by using a waitGroup

1
2
waitGroup := &sync.WaitGroup{}
waitGroup.Add(maxGoRoutines)

Now let’s define a simple go routine. We’ll pass a channel to let us know when to quit, a waitGroup to indicate that we’ve quit once we’ve left the routine, and an identifier to distinguish between go routines to make our demo look cool!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go func(shutdownChannel chan bool, waitGroup *sync.WaitGroup, id int) {
  log.Println("Starting work goroutine...")
  defer waitGroup.Done()

  for {
      /*
      * Listen on channels for message.
      */
      select {
      case _ = <-shutdownChannel:
          log.Printf("Received shutdown on goroutine %d\n", id)
          return

      default:
      }

      // Do some hard work here!
  }
}(shutdownChannel, waitGroup, i)

Once we’ve launed the routines, we’ll wait for the program to terminate. We’ve established a signal handler to let us know when SIGTERM or SIGQUIT by the following lines:

1
2
quitChannel := make(chan os.Signal)
signal.Notify(quitChannel, syscall.SIGINT, syscall.SIGTERM)

Next, we’ll wait to receive a signal that we’ve quit by blocking on the quitChannel. Once we receive a message indicating that we’ve quit, we’ll send a boolean true to our go routine shutdownChannel. Notice that we have to send as many messages to this channel as we have go routines. Otherwise, we’ll leave go routines hanging around and that will block us from terminating.

And finally, we wait for the waitGroup to complete. After each go routine calls its defered waitGroup.Done() function, we will unblock on the waitGroup.Wait() and can successfully exit!

1
2
        waitGroup.Wait()
        log.Println("Done.")

Here’s the whole thing from soup to nuts!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package main

import (
        "log"
        "os"
        "os/signal"
        "runtime"
        "sync"
        "syscall"
)

const (
        maxGoRoutines = 50
)

func main() {

        runtime.GOMAXPROCS(int(float64(runtime.NumCPU()) * 1.25))

        log.Println("Starting application...")

        /*
         * When SIGINT or SIGTERM is caught write to the quitChannel
         */
        quitChannel := make(chan os.Signal)
        signal.Notify(quitChannel, syscall.SIGINT, syscall.SIGTERM)

        shutdownChannel := make(chan bool)
        waitGroup := &sync.WaitGroup{}

        waitGroup.Add(maxGoRoutines)

        /*
         * Create a goroutine that does imaginary work
         */
        for i := 0; i < maxGoRoutines; i++ {
                go func(shutdownChannel chan bool, waitGroup *sync.WaitGroup, id int) {
                        log.Println("Starting work goroutine...")
                        defer waitGroup.Done()

                        for {
                                /*
                                 * Listen on channels for message.
                                 */
                                select {
                                case _ = <-shutdownChannel:
                                        log.Printf("Received shutdown on goroutine %d\n", id)
                                        return

                                default:
                                }

                                // Do some hard work here!
                        }
                }(shutdownChannel, waitGroup, i)
        }

        /*
         * Wait until we get the quit message
         */
        <-quitChannel

        log.Println("Received quit. Sending shutdown and waiting on goroutines...")

        for i := 0; i < maxGoRoutines; i++ {
                shutdownChannel <- true
        }

        /*
         * Block until wait group counter gets to zero
         */
        waitGroup.Wait()
        log.Println("Done.")
}

Comments