こんにちは。KOKUKIです。
最近、Go言語で並行処理を学んでいます。Go言語にはGoroutineという処理を並行で走らせられる機能がデフォルトでついていて、かなり学びやすいです。
Contextパッケージは、並行処理のキャンセル処理とかにかなり便利そうだったので、学習記録としてここに残しておきます。
<目次>
WithCancel
最初は、WithCancelを確認してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
package main import ( "context" "fmt" ) func main() { // 3. コンテキストを受け取る関数を用意 // 戻り値はint型のチャネル // この関数はmain関数の外に定義しても良い generator := func(ctx context.Context) <-chan int { // 4. output用のchannelを用意 out := make(chan int) // 5. カウント用の変数 n := 1 // 6. 無名関数でゴルーチンを起動 go func() { // 10. チャネルを必ず閉じる defer close(out) // 7. for文で無限ループ for { // 8.select文でチャネルごとの分岐を作る select { case out <- n: n++ // 9. cancel()が実行されたらループを抜ける case <-ctx.Done(): return } } }() return out } // 1. コンテキストを作る // Backgroundは空のコンテキストを作る // このコンテキストを介して、他のゴルーチンに干渉する ctx, cancel := context.WithCancel(context.Background()) // 2. コンテキスト付で子ゴルーチンを起動する ch := generator(ctx) // 11. 結果を取り出す for n := range ch { fmt.Println(n) // 12. 特定の条件でcancelを行う(ここでは5になったら処理をキャンセルする) if n == 5 { cancel() } } } |
Contextは、子ゴルーチンにメインゴルーチンからの情報を伝搬させることができるようです。
そのため、最初にcontext.Backgroundで空のContextを作って、そのContextで情報のやりとりを行います。
上記の例では、作成したContextをWithCancelメソッドでラップして、戻り値としてcancel関数を受け取っています。
このcancel関数を実行するとContextを渡した子ゴルーチン側で検知ができるようですね。ただし検知するためには、子ゴルーチン側で<-ctx.Done()を実行する必要があります。
こういう場合は、Select文が便利で、caseを使うことでチャネルごとの分岐ができるので、caseの一つに<-ctx.Done()を定義してcancel実行を待ち受けています。
プログラムを実行してみましょう。
1 2 3 4 5 6 7 |
$ go run main.go 1 2 3 4 5 6 |
6が出力されたところで、プログラムが停止しました。
以下のように<-ctx.Done()をコメントアウトすると無限ループになるので、一度試して見てください。
1 2 3 4 5 6 |
case out <- n: n++ // 9. cancel()が実行されたらループを抜ける // case <-ctx.Done(): // return } |
WithDeadline
次はWithDeadlineを見ていきましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
package main import ( "context" "fmt" "time" ) func main() { // 1. 制限時間を儲ける // プログラムを実行してから1秒以内 deadLine := time.Now().Add(100 * time.Millisecond) // 2. 空のコンテキストを作る c := context.Background() // 3. コンテキストをWithDeadlineでラップする // その際に制限時間も渡す ctx, cancel := context.WithDeadline(c, deadLine) // 4. mainを抜けると同時にcancelを実行する defer cancel() // 5. 制限時間内にHelloを返す関数を作成 sayHello := func() <-chan string { ch := make(chan string) go func() { defer close(ch) // 6. 制限時間を取り出す deadLine, ok := ctx.Deadline() if ok { // 7. 制限時間チェック(50ミリセック以内か) if deadLine.Sub(time.Now().Add(50*time.Millisecond)) < 0 { fmt.Println("Oh, No. Time Over...") return } } // 7. ダミーの作業 time.Sleep(50 * time.Millisecond) // 8. 値受け渡し select { case ch <- "Hello": case <-ctx.Done(): return } }() return ch } ch := sayHello() text, ok := <-ch if ok { fmt.Println(text) } } |
WithDeadlineはその名の通り、制限時間を儲けることができるメソッドです。
「deadLine := time.Now().Add(100 * time.Millisecond)」で100ミリセック以内の制限時間を定義しています。
sayHello関数では50ミリセック以内で動作が完了するので、このままプログラムを実行するとHelloが返されます。
1 2 |
$ go run main.go Hello |
続いて、「deadLine := time.Now().Add(10 * time.Millisecond)」に変更したらどうなるでしょうか。
1 2 |
$ go run main.go Oh, No. Time Over... |
はい。TimeOverのになりましたね。これは結構便利な機能かもしれません。
WithTimeout
WithTimeoutは、コンテキストにタイムアウトを設定します。その名の通りですね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
package main import ( "context" "io" "log" "net/http" "os" "time" ) func main() { // 1. google.comにアクセスするリクエストを作成 req, err := http.NewRequest("GET", "https://google.com", nil) if err != nil { log.Fatal(err) } // 2. コンテキスト共にタイムアウトを設定 ctx, cancel := context.WithTimeout(req.Context(), 10*time.Millisecond) // 3. 制限時間が経過したらcancel defer cancel() // 4. コンテキストをラップする req = req.WithContext(ctx) // 5. リクエストを実行 resp, err := http.DefaultClient.Do(req) if err != nil { log.Println("ERROR:", err) return } defer resp.Body.Close() // 6. 制限時間以内ならリクエスト結果をコンソールに表示 io.Copy(os.Stdout, resp.Body) } |
このサンプルでは、google.comに対してリソースのリクエストを行なっています。
その際に、「ctx, cancel := context.WithTimeout(req.Context(), 10*time.Millisecond)」似て、10ミリ秒以内のタイムアウトを設けました。
つまり、10ミリ秒以内に処理が完了しないとリソースが取得できないことになります。
プログラムを実行してみましょう。
1 2 |
$ go run main.go 2020/10/27 17:23:21 ERROR: Get "https://google.com": context deadline exceeded |
タイムアウトしてくれましたね。
続いて「tx, cancel := context.WithTimeout(req.Context(), 10000*time.Millisecond)」のように10秒にしたらどうなるでしょうか。
1 2 |
go run main.go <!doctype html><html itemscope="" itemtype="http://schema.org/WebPage" lang="ja"><head><meta content="世界中のあらゆる,,, |
今度はちゃんと取れましたね。
指定時間内に処理が終わるか確認したい時なんかに便利そうですね。
WithValue
WithValueも便利そうです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
package main import ( "context" "fmt" ) // 1. 仮のデータベース type database map[string]bool // 2. keyの型情報を定義 type userIDKeyType string // 3. データベース初期化 var db database = database{ "selfnote": true, } func main() { // 4. コンテキスト作成 ctx, cancel := context.WithCancel(context.Background()) // 5. コンテキストをクローズ defer cancel() processRequest(ctx, "selfnote") } func processRequest(ctx context.Context, userid string) { // 6. コンテキストにKey Valueをセット vctx := context.WithValue(ctx, userIDKeyType("userIDKey"), userid) // 7. 値付きコンテキストを渡す ch := checkMemberShip(vctx) // 8. 結果を受け取れるまでブロック status := <-ch fmt.Printf("membership status of userid : %s : %v\n", userid, status) } func checkMemberShip(ctx context.Context) <-chan bool { // 9. 判定用のchannelを作成(DBに存在するか) ch := make(chan bool) go func() { defer close(ch) // 10. コンテキストから値を取り出す // interfaceが返却されるので、stringでキャストする userid := ctx.Value(userIDKeyType("userIDKey")).(string) // 11. IDを検索 status := db[userid] // 12. 結果を格納 ch <- status }() return ch } |
WithValueは、goroutine間で値の受け渡しを可能にします。
上記のサンプルでは、仮想のデータベース(ローカルキャッシュでもいいです)から任意のKey(userIDKeyType)の情報が存在するかgoroutineを使って判定しています。
データ量が少ないので、有効性を感じないかもしれませんが、ひとまずプログラムを実行してみましょう。
Keyにはselfnoteを渡しています。
1 2 |
$ go run main.go membership status of userid : selfnote : true |
trueが返却されましたね。
続いて、keyをhogeに変えてみましょう。
1 |
processRequest(ctx, "hoge") |
プログラムを実行します。
1 2 |
$ go run main.go membership status of userid : hoge : false |
OKですね。
これは仕事で使うかもしれないので、マスターしておきたいですね。
終わりに
Contextパッケージは面白いですね。プログラムの幅が広がっていく感覚がして楽しいです^^
もっと色々なことができるようになって、世の中にたくさんのサービスを送り出したいですね。
それでは、また!
最近のコメント