お題
前回の続き。
今回は、チャネル使ってあれこれ。
ただ、今回も浅いです。
関連記事Index
開発環境
# OS
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="17.10 (Artful Aardvark)"
# 言語
$ go version
go version go1.10 linux/amd64
実践
■2つのチャネル間でやりとり
$共通に使う関数$
ループ内でチャネル「ch」からの入力を待つ。
入力があったら指定のワード「word」を出力して100ミリ秒スリープ。
スリープ後にチャネル「ch」に(他のgoroutine
(自身かもしれないけど)の処理を発火させるために)空の構造体を入力。
func sub(word string, ch chan struct{}, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 5; i++ {
select {
case <-ch:
fmt.Println(word)
time.Sleep(100 * time.Millisecond)
ch <- struct{}{}
}
}
}
No.01: 待ち合わせピンポン(失敗版)
func main() {
wg := &sync.WaitGroup{}
ch := make(chan struct{}, 1)
wg.Add(1)
go sub("ping", ch, wg)
wg.Add(1)
go sub(" pong", ch, wg)
wg.Wait()
}
これ、失敗します。
$ go run main.go
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc42008601c)
/usr/lib/go-1.10/src/runtime/sema.go:56 +0x39
sync.(*WaitGroup).Wait(0xc420086010)
/usr/lib/go-1.10/src/sync/waitgroup.go:129 +0x72
main.main()
/work/src/golang/src/github.com/sky0621/tips-go/goroutine/no03/no0301/main.go:20 +0x11c
goroutine 18 [chan receive]:
main.sub(0x4b372b, 0x4, 0xc42007a060, 0xc420086010)
/work/src/golang/src/github.com/sky0621/tips-go/goroutine/no03/no0301/main.go:28 +0x6c
created by main.main
/work/src/golang/src/github.com/sky0621/tips-go/goroutine/no03/no0301/main.go:15 +0xb6
goroutine 19 [chan receive]:
main.sub(0x4b3d63, 0x9, 0xc42007a060, 0xc420086010)
/work/src/golang/src/github.com/sky0621/tips-go/goroutine/no03/no0301/main.go:28 +0x6c
created by main.main
/work/src/golang/src/github.com/sky0621/tips-go/goroutine/no03/no0301/main.go:18 +0x10e
exit status 2
なぜか?
メインから(チャネルへの入力待ちを行う)2つのgoroutine
を起動したものの、肝心のチャネルへの入力が一切ないから(だと思う)。
ちなみに、sub
関数のselect
にdefault
を書いてやれば↑のエラーは起きなくなる。
func sub(word string, ch chan struct{}, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 5; i++ {
select {
case <-ch:
fmt.Println(word)
time.Sleep(100 * time.Millisecond)
ch <- struct{}{}
ここ-> default:
ここ-> fmt.Println("skip")
}
}
}
すると、こうなる。
$ go run main.go
skip
skip
skip
skip
skip
skip
skip
skip
skip
skip
この手法は、チャネルへの入力があるたびに何度でも発動する処理にしたい時に使える。
(for
を無限ループにしておくこととセット)
No.02: 待ち合わせピンポン(成功版)
目的は2つのgoroutine
間でピンポン打ち合うことなので、No.01での失敗を修正する。
と言っても、No.01で書いた通り、2つのgoroutine
が機能しだすためのきっかけをメイン関数で与えるだけ。
func main() {
wg := &sync.WaitGroup{}
ch := make(chan struct{}, 1)
wg.Add(1)
go sub("ping", ch, wg)
wg.Add(1)
go sub(" pong", ch, wg)
ここ-> // 最初のきっかけを与えてやる
ここ-> ch <- struct{}{}
wg.Wait()
}
すると、2つのgoroutine
が動き出す。
$ go run main.go
pong
ping
pong
ping
pong
ping
pong
ping
pong
ping
No.03: 待ち合わせピンポンパン
念の為、起動するgoroutine
を5つにしても機能することを確認する。
func main() {
wg := &sync.WaitGroup{}
ch := make(chan struct{}, 1)
words := []string{
"ping",
" pong",
" pang",
" peng",
" pung",
}
for _, word := range words {
wg.Add(1)
go sub(word, ch, wg)
}
// 最初のきっかけを与えてやる
ch <- struct{}{}
wg.Wait()
}
$ go run main.go
pung
pong
ping
peng
pang
pung
pong
ping
peng
pang
pung
pong
ping
peng
pang
pung
pong
ping
peng
pang
pung
pong
ping
peng
pang
No.04: 待ち合わせないピンポンパン
「待ち合わせ」る実装の例としてピンポンしていたけど、この事例は本来であれば互いの処理を待ち合わせる必要はまったくない。
というわけで、互いを待たずして単純に平行に走らせるとこうなる。
sub
関数からselect
を除去。
func sub(word string, ch chan struct{}, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 5; i++ {
fmt.Println(word)
time.Sleep(100 * time.Millisecond)
}
}
結果はNo.03と同じく5種類のワードを5回ずつ出力するけど、当然、No.03より処理時間が短い。(互いを待たないから)
$ go run main.go
pung
pong
pang
ping
peng
pong
pung
ping
pang
peng
pong
peng
pung
ping
pang
pung
pang
pong
ping
peng
pung
pang
pong
peng
ping
まとめ
浅いレベルの話を続けているけど、少しずつ応用編を書いていければと思う。
もっといろんなパターンをいち早く知りたい人は↓を見るといいと思う。
https://mattn.kaoriya.net/software/lang/go/20180531104907.htm
https://mattn.kaoriya.net/software/lang/go/20160706165757.htm
https://mattn.kaoriya.net/software/lang/go/20171221111857.htm