非同期処理をキャンセルする機構としてよく使う context.Context
Contextは親子関係が作れて、親をキャンセルするとその子孫も一緒にキャンセルされる。 一緒にといっても、親のキャンセルがトリガーなので、親がわずかに先にキャンセルされると考えるのが自然。
では、select { }
をつかって親子両方のキャンセルを待ち受けてみた。
package main import ( "context" "log" ) func main() { pctx, pcancel := context.WithCancel(context.Background()) cctx, _ := context.WithCancel(pctx) go func() { pcancel() }() select { case <-pctx.Done(): log.Printf("parent done: %+v", pctx.Err()) case <-cctx.Done(): log.Printf("child done: %+v", cctx.Err()) } }
意外な結果に
何度も実行すると、予想に反して親のキャンセルを先に受け取る場合と、子のキャンセルを先に受け取る場合とバラバラになった。
2020/01/28 13:32:09 child done: context canceled 2020/01/28 13:32:09 child done: context canceled 2020/01/28 13:32:09 parent done: context canceled 2020/01/28 13:32:09 parent done: context canceled 2020/01/28 13:32:09 parent done: context canceled 2020/01/28 13:32:09 child done: context canceled 2020/01/28 13:32:09 child done: context canceled
実際に親の内部の done channel は先にcloseされるのだが、キャンセル処理全体にロックがかけらててて、ロックの中で子Contextのキャンセルもやっている。
結果として、pctx.Done()
から値を取り出すのはロック解除の後になるので、そのときは既に子contextもキャンセル済というわけみたい。
// cancel closes c.done, cancels each of c's children, and, if // removeFromParent is true, removes c from its parent's children. func (c *cancelCtx) cancel(removeFromParent bool, err error) { if err == nil { panic("context: internal error: missing cancel error") } c.mu.Lock() if c.err != nil { c.mu.Unlock() return // already canceled } c.err = err if c.done == nil { c.done = closedchan } else { close(c.done) } for child := range c.children { // NOTE: acquiring the child's lock while holding parent's lock. child.cancel(false, err) } c.children = nil c.mu.Unlock() if removeFromParent { removeChild(c.Context, c) } }
ロックが解除されたあとに select { }
に親子どちらの Done()
が先にくるかはタイミングによって変わるということなのかな。