The Go Programming Language chapter 8
並行プログラミング、重要だよね。モバイルアプリはネットワーク通信の間アニメーションしたりする。並行性はI/Oのレイテンシを隠すために使われモダンなコンピューターのプロセッサを消費する。
Goは二つのスタイルの並行プログラミングを可能にしている。この章ではCommunicating Sequential Processes(CSP)をサポートする goroutine
と channels
を紹介する。
9章ではより伝統的なモデルの共有メモリマルチスレッドの一面をカバーしている。9章はさらにこの章で掘り下げなかった並行プログラミングの落し穴にも触れる。
Goの並行性のサポートはそれの強みとはいえ、内在的に逐次プログラムと比較して並行プログラムは難しいし。逐次プログラムでの直感は頼りにならなかったりする。こういうのが初めてなら、この章と次の章に時間かけてみて。
Goにおいて並行実行アクティビティはgoroutineと呼ぶ。 二つの関数を持つプログラムを考えてみよう、一つは計算をし、もう一つは何かに書き出す。互いに呼び出しあわないという仮定の元で。 逐次プログラムでは一つの関数が終ってから次の関数を呼ぶ。並行プログラムでは二つかそれ以上の goroutineを同時に有効化する。
goroutineはスレッドに近い。スレッドとgoroutineの差は定量的な物で性質では無い、9.8章で説明する。
プログラム起動時、goroiutineは main
関数を呼ぶ1個だけ存在する、これを main goroutine と呼ぶ。新しいgoroutineはgo
宣言によって生成される。
f() // fが終るまで待つ
go f() // 新規goroutineでfを実行、待たない
次のサンプルは、フィボナッチの計算をしている間、ヴィジュアルエフェクトを回す。
ch8/spinner
main関数がreturnする時、全てのgoroutineはterminateされプログラムは終了する。(good) goroutineを他から止める方法は無いが、コミュニケートして停止リクエストを送る方法は後程。
ネットワーキングは並行性を使う自然なドメインだ、なぜならサーバーは一度に多くのクライアントとの接続をハンドルするし、それぞれの接続は独立している。この章では net
パッケージを紹介する。TCP, UDP, Unixドメインソケットが使えるやつ。
最初のサンプルは逐次的な時計サーバー。
gopl.io/ch8/clock1
- net.Listenで接続を待ちうける
- listener.Acceptでクライアントの接続要求を受けつけ
- net.Connに現在時刻をwriteStringしつづける (無限ループ)
このサンプルは、同時に一つのクライアント接続しか受けつけない
$ nc localhost 8000
08:58:40
08:58:41
08:58:42
08:58:43
08:58:44
08:58:45
08:58:46
telnetでもなんでもいいけど、localhost:8000に接続すると時刻が流れてくる。
次のコードはRead Only TCPクライアント。
gopl.io/ch8/netcat1
net.Dialでクライアントが接続する。
clock1は同時に1つの接続しかハンドルできなかったが、これに僅かな変更を加えるだけで複数の接続を扱えるようになる。
// before
// handleConn(conn)
// after
go handleConn(conn)
clock2を改造してこんな感じに使う物を作れ
$ TZ=US/Eastern ./clock2 -port 8010 &
$ TZ=Asia/Tokyo ./clock2 -port 8020 &
$ TZ=Europe/London ./clock2 -port 8030 &
$ clockwall NewYork=localhost:8010 London=localhost:8020 Tokyo=localhost:8030
サーバー側
var port = flag.Int("port", 8000, "Listen port number")
...
flag.Parse()
listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
クライアント
package main
import (
"flag"
"io"
"log"
"net"
"os"
"fmt"
"strings"
"time"
)
type locationTimeWriter struct {
w io.Writer
locationName string
}
func (w *locationTimeWriter) Write(data []byte) (int, error) {
return w.w.Write(data)
//return w.w.Write([]byte(w.locationName + string(data)))
}
func main() {
flag.Parse()
fmt.Println(flag.Args())
for _, remote := range flag.Args() {
var name_addr = strings.Split(remote, "=")
var name = name_addr[0]
var addr = name_addr[1]
go handleConn(name, addr)
}
for {
time.Sleep(100 * time.Millisecond)
}
}
func handleConn(name string, address string) {
conn, err := net.Dial("tcp", address)
fmt.Printf("Connected [%s] %s\r\n", name, address)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
var out = &locationTimeWriter{w: os.Stdout, locationName: name}
mustCopy(out, conn)
fmt.Println("OK")
}
func mustCopy(dst io.Writer, src io.Reader) {
if _, err := io.Copy(dst, src); err != nil {
log.Fatal(err)
}
}
サーバー側
- Acceptした接続毎に入力毎のechoを返す (goroutineで並列化)
クライアント側
- サーバーからのレスポンスをStdoutに書く (goroutineで並行して動く)
- 標準入力をソケットに書く