先日のブログ記事は、Go1.24から試験的に導入されたtesting/synctestパッケージについて説明するものでしたが、#blocking-and-the-bubbleに書かれているdurably blockedという概念についてよくわからなかったので、具体例を通して理解してみました。自分用の雑メモでござる。
注意 本記事はバージョンgo1.24.0のtesting/synctestパッケージを対象にしています。
TL;DR
- 「バブルが永続的にブロックされる」って何?
- 具体例で慣れる
前提
本記事を読む上で必要となるtesting/synctestパッケージについての知識をまとめます。
testing/synctest.Run
func Run(f func())
- Run関数は第1引数fを新しいゴルーチンとして実行する
- f自身と、fから推移的に開始されるゴルーチンをまとめたものをバブルと呼ぶ
- Run関数はバブル中の全ゴルーチンが完了するまで制御を返さない
- バブル中では、2000-01-01T00:00:00Zを始まりとする偽の時間(synthetic time)が流れている
- バブルがデッドロックする(<-わからん1) とpanicする
testing/synctest.Wait
func Wait()
特徴
- バブル中に存在している、Wait関数を呼び出したゴルーチン以外の全てのゴルーチンが永続的にブロックされる(durably blocked)(<-わからん2) まで、Wait関数は制御を返さない
- Wait関数を呼び出したゴルーチン以外にゴルーチンが存在しない場合、Wait関数は制御を返す
- 1つのバブル中で同時に複数のWait関数を呼ぶことはできない
わからない用語についての説明
バブル中の1つのゴルーチンが永続的にブロックされるとは
バブル中のゴルーチンが以下のいずれかによってブロックされること。
- nilチャンネルへのメッセージ送受信1
- バブル中で作成されたチャンネルにて発生しているブロック
- 全てのcase文がブロックされているselect構文
- time.Sleep関数
- sync.Cond.Wait関数
- sync.WaitGroup.Wait関数
バブルがデッドロックする
「バブルが永続的にブロックされた」とは「バブル中の全ゴルーチンが永続的にブロックされた」ことを指します。
もしバブルが永続的にブロックされた場合、バブルは次のように振る舞う。
- もし未処理のWait関数がある場合、制御を返す
- そうでない場合、偽の時間が次へ進む(時間が進むことを待っている関数があれば、その関数は制御を返す)
- そうでない場合、バブルはデッドロックとなり、Run関数はpanicする(<-これがバブルのデッドロック)
具体例
先日のブログ記事にある平行プログラムに対するテスト
func TestAfterFunc(t *testing.T) {
synctest.Run(func() { // ゴルーチン1
ctx, cancel := context.WithCancel(context.Background())
funcCalled := false
context.AfterFunc(ctx, func() { // ゴルーチン2
funcCalled = true
})
synctest.Wait() // (1)
if funcCalled { // (2)
t.Fatalf("AfterFunc function called before context is canceled")
}
cancel() // (3)
synctest.Wait() // (4)
if !funcCalled {
t.Fatalf("AfterFunc function not called after context is canceled")
}
})
}
実行過程の解説
(1)が実行された時点において、ゴルーチン1の他に実行されているゴルーチンは存在しない(ゴルーチン2はまだ実行されていない)。この場合、synctest.Waitは制御を返す2。
(2)ゴルーチン2はまだ実行されていないためfuncCalledはfalseとなる。
(3) cancelする。するとゴルーチン2が実行される。
(4) ゴルーチン2が実行中、バブルは永続的にブロックされている状態ではないため、synctest.Waitは制御を返さない。ゴルーチン1が実行完了すると、ゴルーチン1の他に実行されているゴルーチンは存在しなくなるため、synctest.Waitは制御を返す。
余談だが(1)のsynctest.Waitは不要では?3
偽の時間が進んで、Wait関数が制御を返す例
問題
次のmain.goの出力はAとBのどちらが正解でしょう?
package main
import (
"fmt"
"testing/synctest"
"time"
)
func main() {
synctest.Run(func() { // ゴルーチン1
fmt.Printf("synthetic time=%s\n", time.Now().Format(time.RFC3339Nano)) // (1)
go func() { // ゴルーチン2
time.Sleep(1 * time.Second) // (2)
fmt.Printf("synthetic time=%s\n", time.Now().Format(time.RFC3339Nano)) // (3)
}()
synctest.Wait() // (10)
fmt.Println("synctest.Wait is called")
})
}
% GOEXPERIMENT=synctest go run ./cmd/how2synctest002/main.go
synthetic time=2000-01-01T09:00:00+09:00
synthetic time=2000-01-01T09:00:01+09:00
synctest.Wait is called
% GOEXPERIMENT=synctest go run ./cmd/how2synctest002/main.go
synthetic time=2000-01-01T09:00:00+09:00
synctest.Wait is called
synthetic time=2000-01-01T09:00:01+09:00
解答
(1)偽の現在時刻の初期値(UTC 2000-01-01 00:00:00)を表示する。
(2)ゴルーチン2はtime.Sleepによって永続的にブロックされる。
(10)ゴルーチン2が(2)へ到達するまでは、Wait関数は制御を返さない。しかしながら、(2)へ到達すると、ゴルーチン1以外の全てのゴルーチンが永続的にブロックされたため、Wait関数は制御を返す(この振る舞いが理解し難い。「全てがブロックされるまで待つ」というのが直感的ではないからだろうか)。
(3)ゴルーチン1が終わった後、全てのゴルーチンが永続的にブロックされたため、偽の時間が次へ進んで、(2)のtime.Sleepが制御を返す。
ということで正解はB。
チャンネルに対する送受信が制御を返さない具体例
問題
main.goはa
,b
,c
,d
をどの順序で出力するでしょう?
package main
import (
"fmt"
"testing/synctest"
)
func main() {
synctest.Run(func() { // ゴルーチン1
ch := make(chan int)
go func() { // ゴルーチン2
fmt.Println("a")
ch <- 1 // (1)
fmt.Println("b")
}()
synctest.Wait() // (2)
fmt.Println("c")
<-ch // (3)
synctest.Wait() // (4)
fmt.Println("d")
})
}
解答
(1)ゴルーチン2は永続的にブロックされる。
(2)ゴルーチン2が(1)まで到達するとsynctest.Waitは制御を返す。
(3)が実行されるとゴルーチン2の(1)が制御を返すため、ゴルーチン2の永続的なブロックが解除される。
(4)ゴルーチン2が完了するまでは、synctest.Waitは制御を返さない。ゴルーチン2が完了するとsynctest.Waitは制御を返す。
ということで
a
c
b
d
チャンネルによるブロックとtime.Sleepによるブロックが混合してる
問題
main.goはa
,b
,c
,d
,e
をどの順序で出力するでしょう?
package main
import (
"fmt"
"testing/synctest"
"time"
)
func main() {
synctest.Run(func() { // ゴルーチン1
ch := make(chan int)
go func() { // ゴルーチン2
ch <- 1 // (2)
fmt.Println("a")
}()
go func() { // ゴルーチン3
time.Sleep(1 * time.Second) // (3)
fmt.Println("b")
}()
synctest.Wait() // (1)
fmt.Println("c")
synctest.Wait() // (4)
fmt.Println("d")
<-ch // (5)
synctest.Wait() // (6)
fmt.Println("e")
})
}
(1)ゴルーチン2の(2),ゴルーチン3の(3)へ到達したら、synctest.Wait関数は制御を返す。
(4)ゴルーチン2の(2),ゴルーチン3の(3)でブロックされたままであるため、synctest.Wait関数は制御を返す。
(5)この行が実行されると、ゴルーチン2の(2)が制御を返す。
(6)ゴルーチン2が完了したら、synctest.Wait関数は制御を返す。
(3)ゴルーチン1が完了したら、time.Sleepは制御を返す。
ということで
c
d
a
e
b
感想
ちょっと慣れが必要...というかわかりにくくね(^^;