Skip to content

Instantly share code, notes, and snippets.

@faiface
Created October 1, 2018 21:15
Show Gist options
  • Save faiface/ac46bb6c512bd2623343da89317f3ba2 to your computer and use it in GitHub Desktop.
Save faiface/ac46bb6c512bd2623343da89317f3ba2 to your computer and use it in GitHub Desktop.

Problem: the context package

More than a year ago, I wrote a blog post titled Context Should Go Away For Go 2 which received a fair amount of support and response. In said blog post, I described reasons why the "context" package is a bad idea because it's too infectious.

As explained in the blog post, the reason why "context" spreads so much and in such an unhealthy fashion is because it solves the problem of cancellation of long-running procedures.

I promised to follow the blog post (which only complained about the problem) with a solution. Considering the recent progress around Go 2, I decided it's the right time to do the follow up now. So, here it is.

Solution: bake the cancellation into Go 2

My proposed solution is to bake cancellation into the language and thus avoiding the need to pass the context around just to be able to cancel long-running procedures. The "context" package could still be kept for the purpose of goroutine-local data, however, this purpose does not cause it to spread, so that's fine.

In the following sections I'll explain how exactly the baked-in cancellation would work.

One quick point before we start: this proposal does not make it possible to "kill" a goroutine - the cancellation is always cooperative.

Examples to get the idea

I'll explain the proposal by a series of short, very contrived examples.

We start a goroutine:

go longRunningThing()

In Go 1, the go keyword is used to start a goroutine, but doesn't return anything. I propose that it returns a function which when called, cancels the spawned goroutine.

So, now we want to cancel the goroutine:

cancel := go longRunningThing()
cancel()

The goroutine got started and then cancelled immediately.

Now, as I've said, the cancellation is a cooperative operation. The longRunningThing function needs to cooperate. How could it look like?

func longRunningThing() {
    select {
    case <-time.After(5 * time.Second):
        fmt.Println("finished")
    }
}

This longRunningThing function does not cooperate. It takes 5 seconds no matter what. Here's how we can improve it:

func longRunningThing() {
    select {
    case <-time.After(5 * time.Second):
        fmt.Println("finished")
    cancelling:
        fmt.Println("cancelled")
    }
}

I propose the select statement receives an additional branch called cancelling which gets triggered whenever the goroutine is scheduled for cancellation, i.e. when the function returned from the go statement gets called.

The above program would therefore print out:

cancelled

What if the long running thing spawns some goroutines inside of it? Does it have to handle their cancellation? No, it doesn't. All goroutines spawned inside a cancelled goroutine get cancelled first and the originally cancelled goroutine starts its cancellation only after all its 'child' goroutines finish.

For example:

func longRunningThing() {
    go anotherLongRunningThing()
    select {
    case <-time.After(5 * time.Second):
        fmt.Println("finished")
    cancelling:
        fmt.Println("cancelled")
    }
}

func anotherLongRunningThing() {
    select {
    case <-time.After(3 * time.Second):
        fmt.Println("child finished")
    cancelling:
        fmt.Println("child cancelled")
    }
}

This time, running:

cancel := go longRunningThing()
cancel()

prints out:

child cancelled
cancelled

Let's say, that instead of in another goroutine, longRunningThing needs to execute anotherLongRunningThing three times sequentially, like this (anotherLongRunningThing remains unchanged):

func longRunningThing() {
    anotherLongRunningThing()
    anotherLongRunningThing()
    anotherLongRunningThing()
}

This time, longRunningThing doesn't even handle the cancellation at all. But, cancellation propagates to all nested calls. Cancelling this longRunningThing would print this:

child cancelled
child cancelled
child cancelled

All anotherLongRunningThing calls get cancelled one by one.

TODO

Details

Other uses of the mechanism

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment