- Notifications
You must be signed in to change notification settings - Fork18.4k
Description
Edit: The latest version of this proposal is#57928 (comment).
This proposal originates in discussion on#36503.
Contexts carry a cancellation signal. (For simplicity, let us consider a context past its deadline to be cancelled.)
Using a context's cancellation signal to terminate a blocking call to an interruptible but context-unaware function is tricky and inefficient. For example, it is possible to interrupt a read or write on anet.Conn
or a wait on async.Cond
when a context is cancelled, but only by starting a goroutine to watch for cancellation and interrupt the blocking operation. While goroutines are reasonably efficient, starting one for every operation can be inefficient when operations are cheap.
I propose that we add the ability to register a function which is called when a context is cancelled.
package context// OnDone arranges for f to be called in a new goroutine after ctx is cancelled.// If ctx is already cancelled, f is called immediately.// f is called at most once.//// Calling the returned CancelFunc waits until any in-progress call to f completes,// and stops any future calls to f.// After the CancelFunc returns, f has either been called once or will not be called.//// If ctx has a method OnDone(func()) CancelFunc, OnDone will call it.func OnDone(ctx context.Context, f func()) CancelFunc
OnDone
permits a user to efficiently take some action when a context is cancelled, without the need to start a new goroutine in the common case when operations complete without being cancelled.
OnDone
makes it simple to implement the merged-cancel behavior proposed in#36503:
func WithFirstCancel(ctx1, ctx2 context.Context) (context.Context, context.CancelFunc) {ctx, cancel := context.WithCancel(ctx1)stopf := context.OnDone(ctx2, func() {cancel()})return ctx, func() {cancel()stopf()}}
Or to stop waiting on async.Cond
when a context is cancelled:
func Wait(ctx context.Context, cond *sync.Cond) error {stopf := context.OnDone(ctx, cond.Broadcast)defer stopf()cond.Wait()return ctx.Err()}
TheOnDone
func is executed in a new goroutine rather than synchronously in the call toCancelFunc
that cancels the context because context cancellation is not expected to be a blocking operation. This does require the creation of a goroutine, but only in the case where an operation is cancelled and only for a limited time.
TheCancelFunc
returned byOnDone
both provides a mechanism for cleaning up resources consumed byOnDone
, and a synchronization mechanism. (See theContextReadOnDone
example below.)
Third-party context implementations can provide anOnDone
method to efficiently scheduleOnDone
funcs. This mechanism could be used by thecontext
package itself to improve the efficiency of third-party contexts: Currently,context.WithCancel
andcontext.WithDeadline
start a new goroutine when passed a third-party context.
Two more examples; first, a context-cancelled call tonet.Conn.Read
using the APIs available today:
// ContextRead demonstrates bounding a read on a net.Conn with a context// using the existing Done channel.func ContextRead(ctx context.Context, conn net.Conn, b []byte) (n int, err error) {errc := make(chan error)donec := make(chan struct{}) // This goroutine is created on every call to ContextRead, and runs for as long as the conn.Read call.go func() {select {case <-ctx.Done():conn.SetReadDeadline(time.Now())errc <- ctx.Err()case <-donec:close(errc)}}()n, err = conn.Read(b)close(donec)if ctxErr := <-errc; ctxErr != nil {conn.SetReadDeadline(time.Time{})err = ctxErr}return n, err}
And withcontext.OnDone
:
func ContextReadOnDone(ctx context.Context, conn net.Conn, b []byte) (n int, err error) {var ctxErr error // The OnDone func runs in a new goroutine, but only when the context expires while the conn.Read is in progress.stopf := context.OnDone(ctx, func() {conn.SetReadDeadline(time.Now())ctxErr = ctx.Err()})n, err = conn.Read(b)stopf() // The call to stopf() ensures the OnDone func is finished modifying ctxErr.if ctxErr != nil {conn.SetReadDeadline(time.Time{})err = ctxErr}return n, err}