Context cancellation

1,239 views
Skip to first unread message

Ian Davis

unread,
Nov 2, 2016, 8:05:14 AM11/2/16
to golan...@googlegroups.com
Hi all,

I'm trying to understand the idioms around cancellation of contexts.
I've read the godoc and the relevant blog
(https://blog.golang.org/context).

Should you always call the cancelFunc of a cancellable context? Or
should it only be called if the operation is terminated before
successful completion? The godoc says:

"Canceling this context releases resources associated with it, so code
should call cancel as soon as the operations running in this Context
complete."

This implies to me that it must always be called.

If so, then how can the Err function report the reason for the
termination of the operation? The godoc says

"Err returns Canceled if the context was canceled"

So if cancel must always be called to release resources, Err will always
return a value of Canceled.

Any thoughts on this?

Axel Wagner

unread,
Nov 2, 2016, 8:13:06 AM11/2/16
to Ian Davis, golang-nuts
From https://godoc.org/context

Failing to call the CancelFunc leaks the child and its children until the parent is canceled or the timer fires. The go vet tool checks that CancelFuncs are used on all control-flow paths.

I'm not sure how it's *meant* to be used, but a way to do it, while adhering to everything you say, is to use
defer cancel()

In that case, a) cancel will always be called at least once, b) if you want to cancel it before, you can. Just call cancel and c) if you call ctx.Err() (e.g. in a return statement) it will return Cancelled iff you called cancel before in the same function.

So, yes. If you get a cancel function, always make sure to call it (you might use go vet for that) at least once and a simple way to ensure that would be defer.


--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts+unsubscribe@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Ian Davis

unread,
Nov 2, 2016, 8:30:19 AM11/2/16
to Axel Wagner, golang-nuts
On Wed, Nov 2, 2016, at 12:12 PM, Axel Wagner wrote:

Failing to call the CancelFunc leaks the child and its children until the parent is canceled or the timer fires. The go vet tool checks that CancelFuncs are used on all control-flow paths.

I'm not sure how it's *meant* to be used, but a way to do it, while adhering to everything you say, is to use
defer cancel()

In that case, a) cancel will always be called at least once, b) if you want to cancel it before, you can. Just call cancel and c) if you call ctx.Err() (e.g. in a return statement) it will return Cancelled iff you called cancel before in the same function.

So, yes. If you get a cancel function, always make sure to call it (you might use go vet for that) at least once and a simple way to ensure that would be defer.

After sending the message I noticed that govet checks that cancel is called, so that does answer the main part of my question.

I think I am not quite grasping the idomatic use of contexts and cancel. I understand that the function that creates the context via WithCancel should call the canceFunc and defer seems the obvious way to do that.

But I'm not understanding the idiomatic way for child and grandchild functions to signal that the overall operation cannot be completed or has partially failed. Should they be calling the cancelFunc or closing the Done() channel or something else?

Axel Wagner

unread,
Nov 2, 2016, 8:57:18 AM11/2/16
to Ian Davis, golang-nuts
AIUI: A child or grandchild function is not supposed to signal that. They can return an error and let the parent cancel, or they can create their own child context WithCancel and cancel that. Context doesn't replace exceptions.

You should consider context a way to implement dynamic scoping. A context is a thing valid for the current stack frame and downwards and should never affect the stack upwards. Which is also why there's the "put values in there, not pointers" rule; if you have a pointer, it might modify an upstack context.

The advantage of that (and context in general) is, that you don't need to worry too much about synchronization and everything; as a context is fundamentally immutable (-ish), it's fine to pass it to separate goroutines, because they can only do stuff with it downstack.

Ian Davis

unread,
Nov 2, 2016, 9:09:37 AM11/2/16
to golan...@googlegroups.com
On Wed, Nov 2, 2016, at 12:56 PM, 'Axel Wagner' via golang-nuts wrote:
AIUI: A child or grandchild function is not supposed to signal that. They can return an error and let the parent cancel, or they can create their own child context WithCancel and cancel that. Context doesn't replace exceptions.

OK I see that. I think I don't understand the purpose of the Err method if the function that creates the context is the one that cancels it or closes the Done channel. It seems that the only purpose would be to detect a Timeout.


You should consider context a way to implement dynamic scoping. A context is a thing valid for the current stack frame and downwards and should never affect the stack upwards. Which is also why there's the "put values in there, not pointers" rule; if you have a pointer, it might modify an upstack context.

The advantage of that (and context in general) is, that you don't need to worry too much about synchronization and everything; as a context is fundamentally immutable (-ish), it's fine to pass it to separate goroutines, because they can only do stuff with it downstack.

Yes, this is what I am trying to do. I was wondering whether the returned cancelFunc is something that could be passed downstream to signal parents that work should be aborted.

Gustavo Niemeyer

unread,
Nov 2, 2016, 6:36:33 PM11/2/16
to Ian Davis, golan...@googlegroups.com
Hello there,

On Wed, Nov 2, 2016 at 11:09 AM, Ian Davis <m...@iandavis.com> wrote:
On Wed, Nov 2, 2016, at 12:56 PM, 'Axel Wagner' via golang-nuts wrote:
AIUI: A child or grandchild function is not supposed to signal that. They can return an error and let the parent cancel, or they can create their own child context WithCancel and cancel that. Context doesn't replace exceptions.

OK I see that. I think I don't understand the purpose of the Err method if the function that creates the context is the one that cancels it or closes the Done channel. It seems that the only purpose would be to detect a Timeout.

Err allows you to do this:

select {
case <-ctx.Done():
        return ctx.Err()
...
}
 
You should consider context a way to implement dynamic scoping. A context is a thing valid for the current stack frame and downwards and should never affect the stack upwards. Which is also why there's the "put values in there, not pointers" rule; if you have a pointer, it might modify an upstack context.

The advantage of that (and context in general) is, that you don't need to worry too much about synchronization and everything; as a context is fundamentally immutable (-ish), it's fine to pass it to separate goroutines, because they can only do stuff with it downstack.
Yes, this is what I am trying to do. I was wondering whether the returned cancelFunc is something that could be passed downstream to signal parents that work should be aborted.
 
No, the purpose of Context is the opposite: fired tasks are free to do whatever they please until it's time to stop, because the boss says so. Once that happens, error propagation happens as usual to tell what went wrong.

The http://gopkg.in/tomb.v2 package works closer to the model you describe.


Ian Davis

unread,
Nov 2, 2016, 7:12:02 PM11/2/16
to golan...@googlegroups.com
On Wed, Nov 2, 2016, at 10:35 PM, Gustavo Niemeyer wrote:
Hello there,

On Wed, Nov 2, 2016 at 11:09 AM, Ian Davis <m...@iandavis.com> wrote:


On Wed, Nov 2, 2016, at 12:56 PM, 'Axel Wagner' via golang-nuts wrote:
AIUI: A child or grandchild function is not supposed to signal that. They can return an error and let the parent cancel, or they can create their own child context WithCancel and cancel that. Context doesn't replace exceptions.


OK I see that. I think I don't understand the purpose of the Err method if the function that creates the context is the one that cancels it or closes the Done channel. It seems that the only purpose would be to detect a Timeout.

Err allows you to do this:

select {
case <-ctx.Done():
        return ctx.Err()
...
}

In this situation, other than a timeout, what would be closing the Done channel? The goroutine that created the context already knows the error since it's the only one that is supposed to call cancel.



 

You should consider context a way to implement dynamic scoping. A context is a thing valid for the current stack frame and downwards and should never affect the stack upwards. Which is also why there's the "put values in there, not pointers" rule; if you have a pointer, it might modify an upstack context.

The advantage of that (and context in general) is, that you don't need to worry too much about synchronization and everything; as a context is fundamentally immutable (-ish), it's fine to pass it to separate goroutines, because they can only do stuff with it downstack.

Yes, this is what I am trying to do. I was wondering whether the returned cancelFunc is something that could be passed downstream to signal parents that work should be aborted.
 
No, the purpose of Context is the opposite: fired tasks are free to do whatever they please until it's time to stop, because the boss says so. Once that happens, error propagation happens as usual to tell what went wrong.

The http://gopkg.in/tomb.v2 package works closer to the model you describe.

Thanks. I've used tomb before and I've played with the context/tomb integration. I guess I need both.


Gustavo Niemeyer

unread,
Nov 2, 2016, 7:20:01 PM11/2/16
to Ian Davis, golan...@googlegroups.com
On Wed, Nov 2, 2016 at 9:11 PM, Ian Davis <m...@iandavis.com> wrote:
On Wed, Nov 2, 2016, at 10:35 PM, Gustavo Niemeyer wrote:
Hello there,

On Wed, Nov 2, 2016 at 11:09 AM, Ian Davis <m...@iandavis.com> wrote:


On Wed, Nov 2, 2016, at 12:56 PM, 'Axel Wagner' via golang-nuts wrote:
AIUI: A child or grandchild function is not supposed to signal that. They can return an error and let the parent cancel, or they can create their own child context WithCancel and cancel that. Context doesn't replace exceptions.


OK I see that. I think I don't understand the purpose of the Err method if the function that creates the context is the one that cancels it or closes the Done channel. It seems that the only purpose would be to detect a Timeout.

Err allows you to do this:

select {
case <-ctx.Done():
        return ctx.Err()
...
}

In this situation, other than a timeout, what would be closing the Done channel? The goroutine that created the context already knows the error since it's the only one that is supposed to call cancel.

The goroutine that created the context closes the channel. The error returned informs the call site why the task was interrupted, and that call site will often not be the calling goroutine. Without it you'd have to return fmt.Errorf("interrupted") or something, which is both more boring and less precise than simply calling ctx.Err().

 
You should consider context a way to implement dynamic scoping. A context is a thing valid for the current stack frame and downwards and should never affect the stack upwards. Which is also why there's the "put values in there, not pointers" rule; if you have a pointer, it might modify an upstack context.

The advantage of that (and context in general) is, that you don't need to worry too much about synchronization and everything; as a context is fundamentally immutable (-ish), it's fine to pass it to separate goroutines, because they can only do stuff with it downstack.

Yes, this is what I am trying to do. I was wondering whether the returned cancelFunc is something that could be passed downstream to signal parents that work should be aborted.
 
No, the purpose of Context is the opposite: fired tasks are free to do whatever they please until it's time to stop, because the boss says so. Once that happens, error propagation happens as usual to tell what went wrong.

The http://gopkg.in/tomb.v2 package works closer to the model you describe.

Thanks. I've used tomb before and I've played with the context/tomb integration. I guess I need both.

Yeah, tomb and context have some overlap, but they cover different needs. Pretty much every use I made of tomb was on an internal implementation detail to organize the required concurrency inside a service, while context is very nice across API boundaries.


Axel Wagner

unread,
Nov 2, 2016, 7:28:09 PM11/2/16
to Gustavo Niemeyer, Ian Davis, golang-nuts
That is actually a great point I haven't thought about and the crux of the matter (sorry for repeating it, but I think it's worth making very explicit):

While cancelFunc can only be called from the goroutine that created the context, Err can be called downstack. Meaning, if I call cancelFunc, a call *might or might not* return Cancelled as an error. It might not actually implement cancellation or the call might have failed or succeeded concurrently with a different (non-)error.

The ctx.Err() return thus allows a "child-function" to signal back whether a cancellation was *successful*; the creator of the context only know that it has been *tried*.

--

Gustavo Niemeyer

unread,
Nov 2, 2016, 7:32:11 PM11/2/16
to Axel Wagner, Ian Davis, golang-nuts
On Thu, Nov 3, 2016 at 1:27 AM, Axel Wagner <axel.wa...@googlemail.com> wrote:
That is actually a great point I haven't thought about and the crux of the matter (sorry for repeating it, but I think it's worth making very explicit):

While cancelFunc can only be called from the goroutine that created the context, Err can be called downstack. Meaning, if I call cancelFunc, a call *might or might not* return Cancelled as an error. It might not actually implement cancellation or the call might have failed or succeeded concurrently with a different (non-)error.

The ctx.Err() return thus allows a "child-function" to signal back whether a cancellation was *successful*; the creator of the context only know that it has been *tried*.

No, that's really not its intent. Cancel means *STOP IT!*, and any goroutines down the chain are supposed to block until they're really done, returning ASAP.

It's a nasty bug to disrespect that, because the call site will assume that the background noise stopped once the function returned, and act accordingly thereafter.


Axel Wagner

unread,
Nov 2, 2016, 7:43:23 PM11/2/16
to Gustavo Niemeyer, Ian Davis, golang-nuts
In a concurrent world, assuming that a call to cancel means that thing actually got cancelled is dubious at best. For example, you could call cancelFunc, then immediately enter a GC pause, the other goroutine finishes their work in a orderly fashion, you unpause and close the underlying channel.
Very normal behavior, perfectly valid code, unpreventable situation. But still, calling cancelFunc doesn't mean it actually had any real effect. This is what I mean by "successful cancellation" (or not).

I don't believe we actually disagree. Though there might be some phrasing issue. Which is now adding more noise and confusion (sorry. I'll shut up now).

Gustavo Niemeyer

unread,
Nov 2, 2016, 7:55:59 PM11/2/16
to Axel Wagner, golan...@googlegroups.com, Ian Davis

You said the function would return whether it got cancelled or not, so I assumed something like err := foo.CallAPI(ctx), which would be awkward to return without actually canceling.

Yes, if the function is documented to return with background activity, it's of course fine. That said, It's extremely helpful to be able to reliably cancel such background activity and wait until it's done, for a number of reasons: side effect control, testing, polite shutdowns, etc.

That's mainly where gopkg.in/tomb.v2 comes from.

Chris Hines

unread,
Nov 3, 2016, 10:33:31 AM11/3/16
to golang-nuts, axel.wa...@googlemail.com, m...@iandavis.com
FWIW, I'm a big fan of gopkg.in/tomb.v2. For similar functionality using contexts consider golang.org/x/sync/errgroup.

Gustavo Niemeyer

unread,
Nov 4, 2016, 12:49:29 AM11/4/16
to Chris Hines, Ian Davis, golan...@googlegroups.com, axel.wa...@googlemail.com


Glad it's being useful, and indeed, that API looks familiar. Nice.


Reply all
Reply to author
Forward
0 new messages