2つのスタイルの並行処理
- CSPモデル
- プロセス間で直接相互通信をしない (?)
- Goでは独立したプロセスとしてゴルーチンを使い、相互通信のためにチャネルを用いる(?)
- 伝統的な共有メモリマルチスレッディング
- 他の言語では一般的にスレッドが使われる
- Ch.9で取り扱う
Goでは並行に実行にしている動作を goroutine と呼ぶ。
- 通常のプログラムでは1つの関数が終わってから次の関数を実行することができる
- 並行なプログラムでは同時に2つ以上の関数を実行することが出来る
- 他の言語のスレッドのようなもの
- スレッドとは質的な違いよりも量的な違いが大きい
- goroutineは大量に実行しても、スレッドよりメモリを食わず軽いため大丈夫 (Ch.9)
- プログラム実行開始時には
main関数が唯一のゴルーチンでメインゴルーチンと呼ぶ。- 新しいゴルーチンは
go文によって作られる。 - 関数・メソッドの前に
goキーワードを付けるだけ
- 新しいゴルーチンは
f() // f()を呼び出し関数の終了を待つ
go f() // 生成された新たなゴルーチンがf()を呼び出す. 関数の終了を待たない-
[45番目のフィボナッチ数を計算し、スピナーを表示する]
- https://github.com/adonovan/gopl.io/blob/master/ch8/spinner/main.go
- スピナー表示とフィボナッチ数の計算はそれぞれ自律した動作
- 別々の関数だけど同時に動いている
-
main関数の実行が終了すると全てのゴルーチンが停止しプログラムが終了する
- あるゴルーチンから他のゴルーチンを直接停止させることはできない
- 他のゴルーチンと通信をし、そのゴルーチン自身で停止するようにすることはできる
-
https://github.com/adonovan/gopl.io/blob/master/ch8/clock1/clock.go
- 毎秒 現在時刻をクライアントに対して返却するサーバー
- net.Listen
- TCP localhost:8000 をリッスンしている net.Listener を返却する
- Acceptメソッドはリクエストを受信するまでブロックする
- リクエストを受信すると、コネクションを表す net.Conn を返却する
- handleConn
- クライアントから1リクエストの最初から最後までを扱う
- time.Now() で現在時刻を書き出す
- net.Conn構造体は io.Writerインターフェースを満たすため、io.WriteStringで直接書き出すことができる
- 書き込みに失敗すると、ループを終了しCloseでそのコネクションを閉じる
- time.Time.Formatメソッド
- 任意の書式で時刻を出力することができる
- ただし決まった時刻のテンプレートがあるので注意(time.Parseも同様)
-
https://github.com/adonovan/gopl.io/blob/master/ch8/netcat1/netcat.go
- 読み込み専用のTCPクライアント
- コネクションからデータを読み込み、EOFまたはエラーが発生するまで標準出力に書き込み続ける
-
最初のclockサーバーに対して、↑のnetcatクライアントを2つ同時に起動しても、同時に1つしか時刻が出力されない。
- サーバーがシーケンシャルなので、一つのクライアントしか処理できない
- handleConn に go キーワードを付けて、独立したゴルーチンで動作するようにし、並行処理が行えるようにする。
-
https://github.com/adonovan/gopl.io/blob/master/ch8/clock2/clock.go
clock1のhandlerConnにgo付けただけ
8.2の時計サーバーでは1リクエストにつき1ゴルーチンを使用した。 このエコーサーバーでは1リクエストにつき複数のゴルーチンを使うようにする。
簡単なエコーサーバーは以下のような形で作ることが出来る
func handleConn(c net.Conn) {
io.Copy(c, c)
c.Close()
}-
https://github.com/adonovan/gopl.io/blob/master/ch8/reverb1/reverb.go
- やまびこのように、大文字 -> 通常 -> 小文字 の3回エコーを返すサーバー
-
https://github.com/adonovan/gopl.io/blob/master/ch8/netcat2/netcat.go
- クライアントの改良版
- サーバーからの出力を表示する箇所は内容は変わらず
- go を付けて新たなゴルーチン内で動作するようにした
- ターミナルの入力をサーバーへ送信するように変更した
このプログラムでは、エコー出力3回が終わらなくても新たな入力を受け付けることができるが、
最初のエコー出力が終わるまで次のエコー出力は始まらない。
これを直すには go キーワードを echo 部分にさらに追加する必要がある。
- https://github.com/adonovan/gopl.io/blob/master/ch8/reverb2/reverb.go
- handleConn 内の echo 呼び出しに go を付けただけ
- go で始まる関数の引数は go 自身が実行された時に評価される
- この場合、input.Text() はメインゴルーチン内で評価される
go というキーワードをつけるだけで並行処理が出来たが、 並行に実行して大丈夫かどうか確認しなくてはならない。 今回の net.Conn では並行にメソッドを実行して平気だったが、多くの型では並行にメソッドを実行することは危険。 次の章では並行性の安全性について重要なコンセプトについて見ていく。
- ゴルーチン ≒ 並行なGoプログラムの動作
- チャネル ≒ ゴルーチン間のコネクション
- あるゴルーチンから他のゴルーチンへ値を送信するための通信手段
- 全てのチャネルが、特定の型のための値の通信手段となる
- 型は チャネルの要素型 という呼び方をする
- チャネルは参照型(mapやsliceと同様)
- nilや同一値との比較が可能
チャネルを作るには、組み込み関数の make を使う
ch := make(chan int) // ch は chan int型チャネルの操作には送信と受信があり、両方とも <- 演算子を使用する。
ch <- x // 送信文
x = <-ch // 代入文内の受信式
<-ch // 受信式. 結果は破棄される組み込み関数の close でチャネルを閉じることができる。
close(ch)閉じたチャネルに送信するとパニックが発生するが、閉じたチャネルからの受信は値がなくなるまで可能。
単にmakeだけで作られたチャネルは「バッファなしチャネル」となる。 makeにチャネルの容量となる第二引数を付けて、1以上の容量を持つ場合は「バッファありチャネル」となる。
-
送信・受信ともに先に行われた方がブロックされる
- 送信の場合は受信を、受信の場合は送信を待ち続ける
- 送信ゴルーチンと受信ゴルーチン間で同期することが可能
- 送信ゴルーチンからチャネルへ値が送られた後、送信ゴルーチンのブロック解除よりも先に値の受信が行われる(ことが保証される?)
-
https://github.com/adonovan/gopl.io/blob/master/ch8/netcat3/netcat.go
- コネクション断を検知しエラーハンドリングしたバージョン
- 値よりも通信の瞬間や発生が重要な場合はメッセージをイベントと呼ぶことがある
- チャネルの要素型に struct{} 型を使い、目的が同期のみであることを強調
チャネルをゴルーチン間を接続し、あるゴルーチンの出力が他のゴルーチンの入力となるようにすることができる。 これをパイプラインと呼ぶ。
3つのゴルーチンを2つのチャネルで接続するイメージ
[Counter] -> [Squater] -> [Printer]
- Counter: 数値を生成する
- Squater: Counterから受け取った数値を二乗する
- Printer: Squaterから受け取った数値を出力する
https://github.com/adonovan/gopl.io/blob/master/ch8/pipeline1/main.go - パイプラインの簡単な例