公式の A Tour of Go ではじめて Go 言語を触ってみました。
基本編
メソッド編
インターフェース編
以下、並行処理について学んだことのメモ。
Goは言語のコア機能の一部として並行処理機能を提供する(!)
goroutine
- Go のランタイムによって管理される軽量なスレッド
- ここでいう”軽量なスレッド”とは、OSによって提供されるスレッドそのものではなく、Goのランタイムが独自に実現している並行処理の仕組み
- 並行処理における性能の序列
- 子プロセス << OSのスレッド << goroutine
- 新しい goroutine の実行:
go f(x, y, z)
-
f, x, y, z
の評価: 実行元の goroutine -
f
の実行: 新しい goroutine
-
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 3; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("goroutine") // 新しい"軽量スレッド(goroutine)"での実行
say("current")
// 出力結果(実行する度に異なる)
// goroutine
// current
// current
// goroutine
// goroutine
// current
// current
// goroutine
// current
// goroutine
// current
// <-- 実行元の goroutine が終了(新しい goroutine の終了を待たない)
}
チャネル
チャネルの用途
- goroutine は同じアドレス空間で実行されるため、共有メモリへのアクセスは必ず同期する必要がある
- チャネルは goroutine 間での同期を可能にする
- チャネルの宣言:
ch := make(chan int)
-
chan
型がある
-
- チャネルを介した値の送受信
-
v
をチャネルch
へ送信する:ch <- v
-
ch
から受信した変数をv
へ割り当てる:v := <-ch
-
- 片方の goroutine が準備できるまで、送受信はブロックされる
- 明確なロックや条件変数なしに goroutine 間の同期を可能にする
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 合計結果をチャネルに送信
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
// int の chan 型を生成
c := make(chan int)
go sum(s[:len(s)/2], c) // 前方半分の合計処理を担当する goroutine
go sum(s[len(s)/2:], c) // 後方半分の合計処理を担当する goroutine
x, y := <-c, <-c // チャネル経由でそれぞれの合計結果を受信する
fmt.Println(x) // -5 (後方半分の合計結果)
fmt.Println(y) // 17 (前方半分の合計結果)
fmt.Println(x+y)
}
バッファ付きチャネル
- チャネルはバッファとして使うことができる
- バッファ付きチャネルの宣言:
ch := make(chan int, 100)
- 第2引数がバッファの長さ
- バッファが詰まった時 => チャネルへの送信をブロック
- バッファが空になった時 => チャネルからの受信をブロック
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 1 // 送信+1 => 計1
ch <- 2 // 送信+1 => 計2
fmt.Println(<-ch) // 受信-1 => 計1
ch <- 3 // 送信+1 => 計2
ch <- 4 // deadlock! (バッファが詰まってしまった)
}
チャネルの close
- 値の送信側は、これ以上の送信する値がないことを示すために、チャネルを閉じることができる:
close(c)
- 値の受信側は、2つ目の戻り値の真偽値をチェックすることで、チャネルが閉じているかどうかを知ることができる:
v, ok := <-ch
- 受信する値がない かつ チャネルが閉じている =>
false
- それ以外 =>
true
- 受信する値がない かつ チャネルが閉じている =>
-
for i := range c
は、チャネルが閉じられるまで繰り返し値を受信し続けようとする - 注意
- 送信側だけがチャネルを閉じるべきであって、受信側は決してチャネルを閉じてはいけない
- 閉じたチャネルに値を送信すると
panic
が生じる
- 閉じたチャネルに値を送信すると
- チャネルはファイルと違って閉じる必要はない
- これ以上の値が来ないことを受信側が知る必要のあるときに閉じる(例:
for i:= range c
を終了したい場合)
- これ以上の値が来ないことを受信側が知る必要のあるときに閉じる(例:
- 送信側だけがチャネルを閉じるべきであって、受信側は決してチャネルを閉じてはいけない
package main
import (
"fmt"
)
// 送信側
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c) // ここで close し忘れると panic!
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
// 受信側
for i := range c { // close するまで値を受信し続ける
fmt.Println(i)
}
}
select
- 送受信の準備ができているチャネルの
case
を実行する - 複数のチャネルで送受信の準備ができている場合、ランダムに1つの
case
が選ばれる - チャネルで送受信の準備ができていると判定できる例:
- 送信
- バッファが詰まっていない
- 受信
- チャネルから値を取り出し可能
- チャネルが閉じられた
- 送信
package main
import "fmt"
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x: // x を c チャネルに送信
x, y = y, x+y
case <-quit: // quit チャネルから値を受信
fmt.Println("quit")
return // 無限ループ終了
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
// goroutine
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0 // quitチャネルに値を送信 => `case <-quit`準備完了
}()
fibonacci(c, quit)
}
// 出力結果
// 0
// 1
// 1
// 2
// 3
// 5
// 8
// 13
// 21
// 34
// quit
- どのチャネルも準備できていない場合、
default
が実行される
package main
import (
"fmt"
"time"
)
func main() {
tick := time.Tick(100 * time.Millisecond)
boom := time.After(500 * time.Millisecond)
for {
select {
case <-tick:
fmt.Println("tick.")
case <-boom:
fmt.Println("BOOM!")
return
default:
fmt.Println(" .")
time.Sleep(50 * time.Millisecond)
}
}
}
// .
// .
// tick.
// .
// .
// tick.
// .
// .
// tick.
// .
// .
// tick.
// .
// .
// BOOM!
おまけ: "goroutine" は「ゴルーチン」?
Goroutine をカナに落とし込む際に「ゴルーチン」と表記するの「うせやろ!?」って思ったけど、英語での発音を確認したら「もう許せるぞオイ!」となった回 #golang
— BitterBamboo (@bitter_bamboo) June 8, 2022
発音の教材が転がってますね。。。https://t.co/p4thvLyzdX