Context cancellation: Is it sufficient to make long-running things interruptible?

344 views
Skip to first unread message

Torsten Bronger

unread,
Dec 19, 2022, 5:01:41 AM12/19/22
to golan...@googlegroups.com
Hallöchen!

The context documentation gives this example:

// Stream generates values with DoSomething and sends them to out
// until DoSomething returns an error or ctx.Done is closed.
func Stream(ctx context.Context, out chan<- Value) error {
for {
v, err := DoSomething(ctx)
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case out <- v:
}
}
}

What if the channel “out” is drained very efficiently? Then, an
arbitrary number of loop iterations could happen before a
cancellation is detected, couldn’t it?

I would additionally check for ctx.Err() != nil somewhere in the
loop. Or is there a reason why this is not necessary?

Regards,
Torsten.

--
Torsten Bronger

Jan Mercl

unread,
Dec 19, 2022, 5:26:15 AM12/19/22
to golan...@googlegroups.com
On Mon, Dec 19, 2022 at 11:01 AM Torsten Bronger
<bro...@physik.rwth-aachen.de> wrote:

> The context documentation gives this example:
>
> // Stream generates values with DoSomething and sends them to out
> // until DoSomething returns an error or ctx.Done is closed.
> func Stream(ctx context.Context, out chan<- Value) error {
> for {
> v, err := DoSomething(ctx)
> if err != nil {
> return err
> }
> select {
> case <-ctx.Done():
> return ctx.Err()
> case out <- v:
> }
> }
> }
>
> What if the channel “out” is drained very efficiently? Then, an
> arbitrary number of loop iterations could happen before a
> cancellation is detected, couldn’t it?

From https://go.dev/ref/spec#Select_statements

""""
2. If one or more of the communications can proceed, a single one that
can proceed is chosen via a uniform pseudo-random selection.
""""

Selecting the send case in the above code, when both cases can
proceed, for N times in a row, should have on average a probability of
2^(-N).

-j

Brian Candler

unread,
Dec 19, 2022, 5:27:18 AM12/19/22
to golang-nuts
On Monday, 19 December 2022 at 10:01:41 UTC Torsten Bronger wrote:
I would additionally check for ctx.Err() != nil somewhere in the
loop. Or is there a reason why this is not necessary?


    // If Done is not yet closed, Err returns nil.
    // If Done is closed, Err returns a non-nil error explaining why:
    // Canceled if the context was canceled
    // or DeadlineExceeded if the context's deadline passed.
    // After Err returns a non-nil error, successive calls to Err return the same error.
    Err() error

Therefore, it's impossible for ctx.Err() != nil unless ctx.Done() is closed - and the loop is already checking for this.

Harris, Andrew

unread,
Dec 19, 2022, 5:34:17 AM12/19/22
to Torsten Bronger, golan...@googlegroups.com
The precise ordering of things is a bit non-deterministic at the fringe. If precise ordering really matters (maybe for cancelling a stream like this, it doesn’t, sometimes it does), a default case in the select statement is really useful, and it also matters what DoSomething does.

From: golan...@googlegroups.com <golan...@googlegroups.com> on behalf of Torsten Bronger <bro...@physik.rwth-aachen.de>
Sent: Monday, December 19, 2022 1:52:36 AM
To: golan...@googlegroups.com <golan...@googlegroups.com>
Subject: [go-nuts] Context cancellation: Is it sufficient to make long-running things interruptible?
 
--
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...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/87zgbj7maj.fsf%40physik.rwth-aachen.de.

Torsten Bronger

unread,
Dec 19, 2022, 6:51:45 AM12/19/22
to golan...@googlegroups.com
Hallöchen!

Harris, Andrew writes:

> The precise ordering of things is a bit non-deterministic at the
> fringe. If precise ordering really matters (maybe for cancelling a
> stream like this, it doesn’t, sometimes it does), a default case in
> the select statement is really useful, and it also matters what
> DoSomething does.

But the problem is not that the select statement may block – and
only then a “default” would change anything. On the contrary, if
the select blocks, I can be sure it detects a cancellation timely.

Torsten Bronger

unread,
Dec 19, 2022, 7:01:39 AM12/19/22
to golan...@googlegroups.com
Hallöchen!

Jan Mercl writes:

> [...]
>
> From https://go.dev/ref/spec#Select_statements
>
> """"
> 2. If one or more of the communications can proceed, a single one that
> can proceed is chosen via a uniform pseudo-random selection.
> """"
>
> Selecting the send case in the above code, when both cases can
> proceed, for N times in a row, should have on average a probability of
> 2^(-N).

I agree with that as long as DoSomething exists with error quickly
in case of a cancellation, so that we can be sure that the context
is still active at the beginning of the select statement.

So this example is correct.

But would you agree that additional checking is necessary if
DoSomething does not have a ctx argument?

Brian Candler

unread,
Dec 19, 2022, 8:21:10 AM12/19/22
to golang-nuts
On Monday, 19 December 2022 at 12:01:39 UTC Torsten Bronger wrote:
But would you agree that additional checking is necessary if
DoSomething does not have a ctx argument?

Given that you're running DoSomething synchronously, it will always run to completion.  So you'll always have a valid data value to send.

Do you want to throw this value away if the context was cancelled in the mean time?  You can do this:

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

I included the second <-ctx.Done() because if out<-v is blocking for a long time, you still might want to be able to cancel

Note that there is a race condition in this code: the context could be cancelled at any time even after DoSomething has completed.  You have to allow that the value v *may or may not* be sent, depending on exactly when the cancellation signal arrived.

Given that you already have a valid value from DoSomething you could decide to send it regardless, and then test for cancellation.  But that depends on the receiver remaining ready to receive this value.

for {
  v, err := DoSomething()

  if err != nil {
    return err
  }
  out <- v
  select {
  case <-ctx.Done():
    return ctx.Err()
  }
}

TBH, I don't like the idea of a function sending or not sending a message on a channel randomly (depending on whether the context was cancelled); it's a recipe for deadlocks IMO.  You should consider either closing the 'out' channel, or sending a termination message down it, to signal that there's no more data.  I would use *that* as the trigger to terminate the receiver, rather than the context cancellation.

Torsten Bronger

unread,
Dec 19, 2022, 10:51:41 AM12/19/22
to golan...@googlegroups.com
Hallöchen!

Brian Candler writes:

> On Monday, 19 December 2022 at 12:01:39 UTC Torsten Bronger wrote:
>
>> But would you agree that additional checking is necessary if
>> DoSomething does not have a ctx argument?
>
> Given that you're running DoSomething synchronously, it will always
> run to completion. So you'll always have a valid data value to send.
>
> Do you want to throw this value away if the context was cancelled in
> the mean time? You can do this:
>
> select {
> case <-ctx.Done():
> return ctx.Err()
> default:
> select {
> case <-ctx.Done():
> return ctx.Err()
> case out <- v:
> }
> }

Yes, this is what I had in mind. (Or something very similar;
instead of the first “case”, an extra check of ctx.Err() at top
level.)

> [...]
>
> TBH, I don't like the idea of a function sending or not sending a
> message on a channel randomly (depending on whether the context
> was cancelled); it's a recipe for deadlocks IMO. You should
> consider either closing the 'out' channel, or sending a
> termination message down it, to signal that there's no more data.
> I would use *that* as the trigger to terminate the receiver,
> rather than the context cancellation.

I have an error channel that unblocks the receiver (and stops it
from expecting more data).

But here I am worried about terminating the *sender* timely. Does
cancellation mean that I should end the current loop whatever it
takes or that I should end ASAP?

Brian Candler

unread,
Dec 19, 2022, 12:01:48 PM12/19/22
to golang-nuts
It depends entirely on your application, and how the various goroutines are wired together with channels.

But as a guiding principle: I'd say that anything which *sends* on a channel should close it when it's finished - whether that be due to context cancellation, or some other reason. Anything that *consumes* from a channel should keep consuming until it's closed. The consumer doesn't need to check for context cancellation, because it will stop when the channel is closed anyway.

If you wrote it so that the consumer independently notices the cancellation signal and stops consuming, then you risk the sending goroutine blocking forever.

Jason E. Aten

unread,
Dec 20, 2022, 3:21:33 AM12/20/22
to golang-nuts
Shutting down goroutines quickly was needed so often that I wrote a package to help me with it. it is called idem, short for idempotent.

It uses the idea of an idempotent Close of a channel to signal that the goroutine should stop. This is because Go will panic if
close a channel more than once (i.e. you wish to stop a goroutine from more than one place).  This is a design flaw in channels, 
but we can work around it by using a mutex.

Since the goroutine that wants the other goroutine to shutdown typically needs to wait until that is done, there is a reciprocal
closing of another channel to indicate that the message has been received and will be acted on.

https://github.com/glycerine/idem/blob/master/halter_test.go#L43

Jan Mercl

unread,
Dec 20, 2022, 3:41:08 AM12/20/22
to Jason E. Aten, golang-nuts
On Tue, Dec 20, 2022 at 9:21 AM Jason E. Aten <j.e....@gmail.com> wrote:

> Shutting down goroutines quickly was needed so often that I wrote a package to help me with it. it is called idem, short for idempotent.
>
> It uses the idea of an idempotent Close of a channel to signal that the goroutine should stop. This is because Go will panic if
> close a channel more than once (i.e. you wish to stop a goroutine from more than one place). This is a design flaw in channels,
> but we can work around it by using a mutex.

I disagree with it being a design flaw. Quite the opposite, IMO.
Closing a channel more than once reveals a design flaw in the
respective program and panicking is about the only safe thing to do in
that situation.

Working around this safety feature should be discouraged.

Jason E. Aten

unread,
Dec 20, 2022, 10:20:15 AM12/20/22
to Jan Mercl, golang-nuts
Nonsense. 

Closing channels is a generic means of broadcasting to all listeners a state change.

To say that only one goroutine should ever initiate such a change is a needless and pointless restriction.

Reply all
Reply to author
Forward
0 new messages