はじめに
先週のGolang Weeklyで流れてきた下記の記事が、groutine
とチャネル
の動きについてわかりやすそうだったので英語の勉強も兼ねて和訳してみました。
変な訳し方もあったら指摘いただけると幸いです。
A visual introduction to golang concurrency and goroutines
和訳中に利用する画像やコードは全て上記からの引用です。
A visual introduction to golang concurrency and goroutines
Golangは、goroutineを利用した驚くべき並行(≠並列)システムとして知られている言語である。
Golangはgoroutineとチャネルによって平行性を実現している。
Prerequisites
- go構文の基本的な理解
What are Goroutines?
goroutineはスレッドとして考えることもできるが、異なる点がいくつかある。
- goroutineは、平均8kB軽い
- goroutineは、チャネルと通信するが、スレッド通信の方が複雑
- goroutineのスケジューリングは、実行時にgoによって管理されている
- goroutineには、アイデンティティがない
- goroutineは、実際のOSスレッド上で実行され、その数は
GOMAXPROCS
によりコントロールされている
Goroutine states
このチュートリアルでは、goroutineが次のうちの1つの状態に常にいる状態と考えると便利である。
- Waiting: goroutineは、ブロッキングステートメントにブロックされていないが、スケジューラによってまだ実行されていない状態。
- Running: goroutineは、スケジューラによって実行されている状態。
- Blocking: goroutineは、ブロッキング操作(チャネルやスリープ、ネットワーク操作等)によってブロックされている。そしてスケジューラは、次に利用可能なgoroutineへと遷移する。
- Executed: goroutineは、完全に実行され、終了する。
Firing a goroutine
このコードを見てください。
func main(){
s := "main"
go side()
for _,item := range s{
fmt.Println(string(item))
}
}
func side(){
s := "goroutine"
for _,item := range s{
fmt.Println(item)
}
}
main
何が起きているか?
side()
がgoroutineとして実行するために呼ばれているが、まだそこからの出力は受け取っていない。なぜか?
goroutine()
はまだready状態ではなく、スケジューラがアプリケーションの実行を通して実行しなかった。
もしこのように変えると:
...
func main(){
s := "main"
go side()
for _,item := range s{
fmt.Println(string(item))
}
time.Sleep(time.Millisecond)
}
...
これが起きる
メインのgoroutineでブロッキングステートメントであるtime.Sleep
が呼ばれ、goroutineがblocked
状態になる。
それはgoスケジューラが、実行する準備ができている状態のスケジュールされたgoroutineを実行することを可能にする。この場合は、goroutine()
が実行される。
これから、スケジュールされるまでgoがgoroutineを実行しないことがわかる。
そして、どのようにgoroutineを実行するのか?他の全てのgoroutineを使うことで、それを実行する、もしくはブロックする前に並べられる。
Multiple goroutines.
下記のコード考える:
func main(){
go sayWord("goroutine1")
go sayWord("goroutine2")
sayWord("Main")
time.Sleep(time.Millisecond)
fmt.Println("End of main")
}
func sayWord(word string){
for i := 0;i< 2; i++ {
fmt.Println(word)
}
}
Main
Main
goroutine1
goroutine1
goroutine2
goroutine2
End of main
Channels Intro
goroutine間のパイプラインとしてチャネルを考える。チャネルは、(1種類の)データを送信でき、そこから読み取ることもできる。
このチュートリアルでは緑矢印がチャネルを表している
Declaring a channel
チャネルは次の構文で生成できる。
pipe := make(chan string)
これでデータを送受信できる。
//Send hello to it
pipe <- "Hello"
go goroutine(pipe)
//Receive hello from it
func goroutine(pipe chan string){
hello := <- pipe
}
Unidirectional Channel
初期化時の矢印の方向で、チャネルをインプットもしくはアウトプット専用としても使うことができる。
func goroutine(in <-chan string, out chan<- string){
received := <-in
out <- "processed"
}
Channels are blocking by default.
データにchannel <- "Hi"
が書きこまれたとき、goroutineは他のgroutineがそのチャネルから読み込むまでブロックされる。
NOTE: もし他のgoroutineが読み込まなければ、デッドロックが起きてプログラムがクラッシュする。
同様に、もし何もないのにチャネルからデータを読み込もうとしたら、goroutineは他のgoroutineがチャネルにデータを送信するまでブロックされる。
下記のコードを考えてみる:
func main() {
pipe := make(chan string)
go sayWord(pipe)
fmt.Println("start of main")
pipe <- "Hello"
fmt.Println("End of main")
}
func sayWord(in <-chan string) {
fmt.Println(<-in)
fmt.Println("Data received in sayWord")
}
start of main
Hello
Data received in sayWord
End of main
-
main
goroutineは、データがバッファがない状態のパイプチャネルに送信されるまで、実行する。(ブロッキング処理) -
sayword
はスケジューラによって実行され、一度main
を読んでいるデータがレディ状態に遷移する。 -
main
はその実行を完了する。
Buffered channels
バッファのあるチャネルはmake()
コマンドの第2引数でバッファサイズを指定することで作成される。
バッファのあるチャネルとないチャネルの違いは、下記を含んでいる。
- バッファのあるチャネルにデータをプッシュしても、データが一杯になるまではgoroutineがブロックされない。
- バッファのないチャネルからのデータ読み込みは、空になるまではノンブロッキング状態となる。
バッファのないチャネルは
make(chan T, 0)
と同じものと考えられる。
Sample code
func main() {
pipe := make(chan int,3)
go consoleLog(pipe)
for i := 0; i < 3; i ++{
pipe <- i
}
fmt.Println("Done")
}
func consoleLog(in <-chan int) {
//Infinite loop to always read channel when running
for{
fmt.Println(strconv.Itoa(<-in))
}
}
Done
見たとおり、goroutineは実行されない。なぜなら、main
関数のforループで3つの値がcapが3のバッファに追加されるだけで、main
のgoroutineはブロックされること無く実行されるため。
4つの値を追加した場合は:
func main() {
pipe := make(chan int,3)
go consoleLog(pipe)
for i := 0; i < 4; i ++{
pipe <- i
}
fmt.Println("Done")
}
...
0
1
2
3
Done
-
main
goroutineは4回目のチャネルへのプッシュでブロックされる。 - コントロールは、チャネルを空にする無限ループを実行する
consoleLog
goroutineに与えられる。 -
consoleLog
goroutineは、空のチャネルから読み込もうとするとブロックする。 - コントロールは、
main
に戻り、残りが実行されて終了する。
Range
range
句はさっきの無限ループの代わりに、下記コードのように使うことができる。
func sayWord(in <-chan int) {
//Infinite loop to always read channel when running
for item := range in{
fmt.Println(strconv.Itoa(item))
}
}
結果はさっきと同じものが出力される。
Select
select
は、チャネルのswitch
と似ている。select
は下記を行う。
- 最初のノンブロッキングのものを実行する。
- もし1つ以上がノンブロッキングであれば、ランダムに1つを実行する。
- 全てがブロックされている場合、いずれかが解除されるまで全てをブロックする。
default
がない限りは。
func main() {
pipe := make(chan string, 2)
second := make(chan string, 2)
go goroutines(second, time.Second*3,"First goroutine")
go goroutines(pipe, time.Second*2,"Second goroutine")
select {
case res := <-pipe:
fmt.Println(res)
case res := <-second:
fmt.Println(res)
}
}
func goroutines(out chan<- string, wait time.Duration, name string) {
time.Sleep(wait)
out <- name
}
Second goroutine
Default case
もし、上のコードを下記の通り更新すると、
func main() {
pipe := make(chan string, 2)
second := make(chan string, 2)
go goroutines(second, time.Second*3,"First goroutine")
go goroutines(pipe, time.Second*2,"Second goroutine")
select {
case res := <-pipe:
fmt.Println(res)
case res := <-second:
fmt.Println(res)
default:
fmt.Println("None are ready")
}
}
実行結果は、None are ready
となるはず。
利用可能なdefault
がなければ、(もし全てがブロックされていたら)select
はブロックする。default
があれば、それがすぐに実行される。
Conclusion.
この記事は、goroutineや、goroutineの通信、それらがどのようにスケジュールされているかを理解するのに役立つだろう。goroutineがチャネルをどのように上手く調整するかを理解できるように、チャネルの内部の動きに関するフォローアップ記事を書く予定である。(どう訳すかわからん...)
それまで、goroutineを試したり遊んだりして、異なる結果を出してみよう。もしくはGOMAXPROCS
をCPUで利用可能なコア数に設定して、並列処理を試してみよう(注意が必要)。
おわりに
上手く訳せなかったところも多く、これからの英語の勉強も必須だなと感じました。
内容自体は、コードや図もあるのでわかりやすく、理解できたと思います。
今までこうした動きは、何となくこう動いているんだろうなとしか考えたことがなかったので新鮮です。
こういった内部ロジックを理解することは、パフォーマンスにも影響していくので、引き続き勉強していきたいです。