Demystifying the Golang Context objects

Golang is a language that was built with concurrency in mind. It makes creating threads trivial and crystal clear, and enables easy inter-thread communication using the channels abstraction:

func main() {
  message := make(chan string)

  go func() {
    fmt.Println("Hello from thread 1! Waiting on a message...")

    receivedMessage := <-message
    fmt.Println("Thread 1 received the following message:", receivedMessage)
  }()

  go func() {
    fmt.Println("Hello from thread 2!")

    fmt.Println("Thread 2 sending message to thread 1...")
    message<-"hello from thread 2"
  }()
}

With Go 1.7, Context objects were added to the language’s standard libraries. These objects push the abstraction even further to manage an application’s thread arborescence.

 Managing a thread tree without Contexts

Say we have a simple application, made of a main thread and of three subthreads. The subthreads print text at regular intervals, and the main thread is tasked with shutting them down after some time. If we want to shut them down in a clean way, we have to establish communication channels between the main channel and the threads.

thread1.png

This is what it could look like:

func printRegularly(text string, timeout time.Duration, stop chan bool) {
  for {
    select {
    case <-time.After(timeout):
      fmt.Println(text)
    case <-stop:
      return
    }
  }
}

func subThreadCreator(text string, timeout time.Duration) chan bool {
  stopChannel := make(chan bool)
  go printRegularly(text, timeout, stopChannel)
  return stopChannel
}

func main() {
  stopChannels := make([]chan bool, 3)
  stopChannel[0] = subThreadCreator("hello from thread 1!", time.Second)
  stopChannel[1] = subThreadCreator("hello from thread 2!", time.Second * 2)
  stopChannel[2] = subThreadCreator("hello from thread 3!", time.Second * 3)

  time.Sleep(15 * time.Second)

  for _, stopChannel := range stopChannels {
    stopChannel <- true
  }
}

Let’s even say one of those subthreads will be a reproduction of our main thread, producing another level of subthreads. It means we have to add another layer and communication, and to ensure transmission of the messages sent.

thread2.png

Following the previous example, we could implement it like this:

func printRegularly(text string, timeout time.Duration, stop chan bool) {
  for {
    select {
    case <-time.After(timeout):
      fmt.Println(text)
    case <-stop:
      return
    }
  }
}

func subThreadCreator(text string, timeout time.Duration) chan bool {
  stopChannel := make(chan bool)
  go printRegularly(text, timeout, stopChannel)
  return stopChannel
}

func multiSubthreadCreator() chan bool {
  selfStop := make(chan bool)

  stopChannels := make([]chan bool, 2)
  stopChannel[0] = make(chan bool)
  stopChannel[1] = make(chan bool)

  go printRegularly("hello from subthread 1!", time.Second * 4, stopChannel[0])
  go printRegularly("hello from subthread 2!", time.Second * 5, stopChannel[1])

  go func() {
    <-selfStop
    stopChannel[0] <- true
    stopChannel[1] <- true
  }()

  return selfStop
}


func main() {
  stopChannels := make([]chan bool, 3)
  stopChannel[0] = subThreadCreator("hello from thread 1!", time.Second)
  stopChannel[1] = subThreadCreator("hello from thread 2!", time.Second * 2)
  stopChannel[2] = multiSubthreadCreator()

  time.Sleep(15 * time.Second)

  for _, stopChannel := range stopChannels {
    stopChannel <- true
  }
}

At this point, it… starts go get messy. Of course, we still have a good communication system between our threads, but there’s more and more overhead. Instead of focusing on what the threads do, we end up focusing on how to stop or control our threads. Although Go channels make the problem easier, they don’t solve it entirely.

That’s where Context objects are useful!

 Managing a context tree with Contexts

The first purpose of a Context object is to indicate a completed action. A Context’s Done() method returns a chan struct{} that sends an object when the thread is to be completed. This fits especially well with Go threads that can often be hanging waiting for multiple channels - in that case, it’s only a matter of adding the <-ctx.Done() case in the select.

What’s also interesting is that Contexts, from the start, are made to be built on top of each other. When you’re creating a Context for a thread, you can also create Subcontexts: when their parent context is done, the subcontexts are done too.

The first Context that you’ll use is the object returned by context.Background(). It’s a context that never expires. However, you’ll build contexts on top of it, that will expire, depending on when you want them to. For example, here are some functions included in the Go standard library:

Contexts can be shared within threads, and are goroutine-safe: you can call them at different places, and when done, will send a message on Done() around the same time.

Let’s see how we can rebuild the second example we had with Contexts:

func printRegularly(ctx context.Context, text string, timeout time.Duration) {
  for {
    select {
    case <-time.After(timeout):
      fmt.Println(text)
    case <-ctx.Done():
      return
    }
  }
}

func multiSubthreadCreator(ctx context.Content) {
  go printRegularly(ctx, "hello from subthread 1!", time.Second * 4)
  go printRegularly(ctx, "hello from subthread 2!", time.Second * 5)
}

func main() {
  ctx, cancel := context.WithCancel(context.Background())
  go printRegularly(ctx, "hello from thread 1!", time.Second)
  go printRegularly(ctx, "hello from thread 2!", time.Second * 2)
  go multiSubthreadCreator(ctx)

  time.Sleep(15 * time.Second)

  cancel()
}

This was much easier! Now, let’s say we want to stop all the subthreads from multiSubthreadCreator 10 seconds after creating them. Let’s create a subcontext!

func printRegularly(ctx context.Context, text string, timeout time.Duration) {
  for {
    select {
    case <-time.After(timeout):
      fmt.Println(text)
    case <-ctx.Done():
      return
    }
  }
}

func multiSubthreadCreator(ctx context.Content) {
  subCtx, cancel := context.WithCancel(ctx)
  go printRegularly(subCtx, "hello from subthread 1!", time.Second * 4)
  go printRegularly(subCtx, "hello from subthread 2!", time.Second * 5)

  time.Sleep(time.Second * 10)

  cancel()
}

func main() {
  ctx, cancel := context.WithCancel(context.Background())
  go printRegularly(ctx, "hello from thread 1!", time.Second)
  go printRegularly(ctx, "hello from thread 2!", time.Second * 2)
  go multiSubthreadCreator(ctx)

  time.Sleep(15 * time.Second)

  cancel()
}

 Passing values through threads with Context values

Contexts are a flexible system to control thread trees. And now that we have context objects flowing through our application, we can also use them to transmit data over an application, through Context values.

Let’s rebuild the example in a way that we don’t want to pass any text to the printing functions. Instead of passing text directly to the function, we can pass a Context value within the context, with some indication on the content to show:

func printRegularly(ctx context.Context, timeout time.Duration) {
  for {
    select {
    case <-time.After(timeout):
      text := "hello world!"
      if threadType, ok := ctx.Value("threadType").(string); ok {
        text = fmt.Sprintf("hello from %s!")
      }
      fmt.Println(text)
    case <-ctx.Done():
      return
    }
  }
}

func multiSubthreadCreator(ctx context.Content) {
  subCtx, cancel := context.WithCancel(ctx)
  subCtx = subCtx.WithValue("threadType", "subthread")
  go printRegularly(subCtx, time.Second * 4)
  go printRegularly(subCtx, time.Second * 5)

  time.Sleep(time.Second * 10)

  cancel()
}

func main() {
  ctx, cancel := context.WithCancel(context.Background())
  ctx = ctx.WithValue("threadType", "thread")
  go printRegularly(ctx, time.Second)
  go printRegularly(ctx, time.Second * 2)
  go multiSubthreadCreator(ctx)

  time.Sleep(15 * time.Second)

  cancel()
}

This context value system presents some overhead, because of the loose typing and of the necessary type-checking operations. However, they can be a good replacement to function parameters, in situations where Context objects control the flow of your threads.

 Final word

Context objects are a welcome new addition to the Go standard library, and fit naturally with the channels system. They’ve quickly been adopted by the general Go community, and many libraries now offer integration with Context objects to manage the lifecycle of an operation. For example, the Go implementation of gRPC has already integrated native Contexts to their RPCs. Now that you know all about contexts, you should be comfortable with using those!

 
0
Kudos
 
0
Kudos

Now read this

Using Docker as a basic Node developing environment

For the past few days, I’ve been working on this small NodeJS project. Node is a very powerful and fast JavaScript runtime environment, and it’s gotten a lot of attention these last few years as a credible back-end for Web... Continue →