contextにおけるキャンセル伝搬の流れ

contextのキャンセル伝搬の流れを調べてみました。
キャンセル処理を行う簡単な例として以下のサンプルコードを用意しました。こちらを元にソースコードから流れを追っていきます。

## サンプルコード

main関数でgoroutinを起動して`sleepFunc`を呼び出し、そのなかでさらにgoroutinを起動して`time.Sleep`を実行しています。

```go
package main

import (
	"context"
	"fmt"
	"time"
)

func sleepFunc(ctx context.Context, duration time.Duration) string {
	doneCh := make(chan struct{})
	go func() {
		time.Sleep(duration * time.Second)
		doneCh <- struct{}{}
	}()

	select {
	case <-doneCh:
		return "done"
	case <-ctx.Done():
		//ここでキャンセル処理を行う
		return "cancel"
	}
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	resultCh := make(chan string)
	go func() {
		resultCh <- sleepFunc(ctx, 10)
	}()
	cancel()

	fmt.Println(<-resultCh)
}
```

`sleepFunc()`内で起動したgoroutinで`time.Sleep()`を実行しますが、`time.Sleep()`が完了する前にmain関数で`cancel()`が呼ばれるため、標準出力に`cancel`と表示されます。
動作は[Go Playground](https://play.golang.org/p/uU1dEedvrhy)で確認できます。

## 結論

`<- ctx.Done()`で待機しているところに対して、`cancelCtx`構造体がもつ`chan struct{}`型の`done`を`close()`することでキャンセルシグナルを伝搬しています。

## contextの取得

```
ctx, cancel := context.WithCancel(context.Background())
```

main関数では最初に`context.Backgroud()`を引数にして`context.WithCancel()`を呼び出してcontextを取得しています。

### `context.Background()`

`context.Background()`は空のcontextである`background`を返す関数です。
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L202-L208

`backgroud`は`emptyCtx`を`new()`で初期化したものです。
`emptyCtx`は`Context`インターフェースを満たすint型の変数ですが、実装しているメソッドのうち`String()`以外のメソッドには実処理がなく、returnするだけになっています。
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L167-L195

### `context.WithCancel()`

`context.WithCancel()`は引数で受け取ったcontextを親としてもつ新たなcontextを返します。
サンプルコードでは`context.Background()`を引数にしていますので、`emptyCtx`を初期化したオブジェクトを渡しているということになります。
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L224-L234

中身をもう少し見ていきます。

`newCancelCtx()`は`cancelCtx`構造体を初期化して返す関数です。
`cancelCtx`には`Context`インターフェースが埋め込まれており、その他にいくつかのフィールドの定義がされています。
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L236-L239
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L314-L323

次の行では`parent`と`&c`を引数に渡して`propagateCancel()`を実行しています。
親contextとそこから派生したcontext(のアドレス)を使って何らかの処理をしていると考えられます。

`propagateCancel()`では、まずはじめに`parent.Done()`が`nil`であるかをチェックして、`nil`である場合はそこでreturnします。
サンプルコードでは、`parent`は`emptyCtx`を初期化したオブジェクトなので`emptyCtx`に実装されている`Done()`メソッドを見ます。これはnilを返すのでここでreturnされます。
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L243-L245

最後に`newCancenCtx()`で取得した新しいcontextのアドレスと、キャンセル用の関数をreturnしています。
これらはmain関数で`ctx`, `cancel`という変数に格納しています。
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L233

## キャンセルの実行

サンプルコードではcontextを取得した後、goroutinを起動して`sleepFunc()`を実行します。このとき引数でcontextを渡しています。

`sleepFunc()`は内部で更にgoroutinを起動して`time.Sleep()`を実行します。
goroutinの外では`select`でcannelの受信を待機しています。`doneCh`は`time.Sleep()`が正常に完了した場合に受信し、`ctx.Done()`はキャンセルが実行された場合に受信します。

main関数でgoroutinを起動した直後に`cancel()`を実行しているので、実際には`ctx.Done()`からシグナルを受信して標準出力に"cancel"という文字列が表示されます。

### `ctx.Done()`

キャンセル伝搬の流れを見る前にまずは`ctx.Done()`の中身を見たいと思います。

`cancelCtx`のフィールドに含まれる`synx.Mutex`を利用してロックを取得します。
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L326
これは定義している箇所のコメントから、他のフィールドを保護する用途で使われていることが読み取れます。
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L319

`c.done`が`nil`である場合は初期化したchannelを`c.done`に代入します。その後`c.done`を`d`に代入してreturnしています。
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L327-L332

なお、channelは参照型なので一旦`d`に代入していますがこれは`c.done`と同じデータ構造を参照しています。
https://golang.org/doc/effective_go.html#channels

> Like maps, channels are allocated with make, and the resulting value acts as a reference to an underlying data structure.

## `cancel()`

キャンセル伝搬の流れです。

`cancel`は`context.WithCancel()`の2つめの返りとして受け取ったものでした。
`context.WithCancel()`の2つめの返り値は`func() { c.cancel(true, Canceled) }`でしたので、キャンセル処理の流れを追うには`cancelCtx`の`cancel()`メソッドの中身を追っていけばよさそうです。

`err == nil`である場合にpanicを発生させています。`err`は第二引数で渡されている`Canceled`です。`errors.New()`で定義したerrorが代入されていて`nil`ではないので、次の処理に進みます。
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L348-L350
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L154-L155

`cancelCtx`のフィールドに含まれる`synx.Mutex`を利用してロックを取得します。
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L351

`c.err != nil`の場合はすでにキャンセル済みとみなされ、ロックを開放したあとにreturnされます。
次の行で`c.err`に`err`を代入していますので、この`cancel()`が実行されると`c.err`が`nil`でなくなり、次の呼び出しのときは`c.err != nil`になるということのようです。
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L352-L356

`c.done == nil`の場合は`closedchan`が代入され、そうでない場合は`c.done`がクローズされます。
あらかじめ`ctx.Done()`を実行していた場合、`c.done`には初期化したchannelがセットされているため、そのchannelがクローズされます。
つまり`case <-ctx.Done():`のようにしていた場合、`c.done`がクローズされたタイミングでシグナルを受信しますので、これによりキャンセルを伝搬しているということになります。
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L357-L361

`closedchan`はここで定義されています。empty structのchannelです。
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L307-L308

`c.children`の各要素の`cancel()`メソッドを実行しています。
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L362-L365

第一引数の`removeFromParent`がtrueである場合に`removeChild()`が実行されます。
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L369-L371

親contextがある場合は、親contextが持つ子contextのmapから自らを削除します。
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L287-L298

## まとめ

`context.WithTimeout()`や`context.WithValue()`を使った場合や複数のgoroutinをキャンセルする場合の流れについても見てみようと思います。

## 参考文献

- https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go
- https://golang.org/pkg/context/
- https://blog.golang.org/context
- https://deeeet.com/writing/2016/07/22/context/