『Go言語で学ぶ並行プログラミング』の練習問題より
今回は読書会じゃなくて Bluesky からのネタ1。
Go言語で学ぶ並行プログラミングの4章の hello world みたいな練習問題、すでにめっちゃ難しいんだけど、この答えの 26行目は lock とらないでいいの? (4.3 練習問題 1) github.com/cutajarj/Con...
— ikawaha (@ikawaha.bsky.social) April 16, 2025 at 11:57 PM
[image or embed]
「4.3 練習問題」の1というのがこれ。
リスト 4.15(もともとは第 3 章から)は、共有変数へのアクセスを保護するためのミューテックスを使っていません。これは悪い習慣です。このプログラムを変更して、共有変数 seconds へのアクセスをミューテックスで保護するようにしてください。ヒント:変数をコピーする必要があるかもしれません。
んで「リスト 4.15」というのがこちら:
package main
import (
"fmt"
"time"
)
func countdown(seconds *int) {
for *seconds > 0 {
time.Sleep(1 * time.Second)
*seconds -= 1
}
}
func main() {
count := 5
go countdown(&count)
for count > 0 {
time.Sleep(500 * time.Millisecond)
fmt.Println(count)
}
}
簡単なカウントダウンのプログラムだけど,カウントダウンを goroutine で実行しているのがポイント。
main()
の count
変数が2つの goroutine で使われているの(countdown()
関数側は seconds
)がお分かりだろうか。
で,この問題の回答は GitHub リポジトリで公開されている。 こんな感じ:
package main
import (
"fmt"
"sync"
"time"
)
func countdown(seconds *int, mutex *sync.Mutex) {
mutex.Lock()
remaining := *seconds
mutex.Unlock()
for remaining > 0 {
time.Sleep(1 * time.Second)
mutex.Lock()
*seconds -= 1
remaining = *seconds
mutex.Unlock()
}
}
func main() {
mutex := sync.Mutex{}
count := 5
go countdown(&count, &mutex)
remaining := count
for remaining > 0 {
time.Sleep(500 * time.Millisecond)
mutex.Lock()
fmt.Println(count)
remaining = count
mutex.Unlock()
}
}
上で強調している行はロック取ってないよね,というのが最初のポストの内容というわけだ。
ちょっとややこしいのだが,もともとこの練習問題の回答としては
func main() {
mutex := sync.Mutex{}
count := 5
go countdown(&count, &mutex)
for count > 0 {
time.Sleep(500 * time.Millisecond)
mutex.Lock()
fmt.Println(count)
mutex.Unlock()
}
}
が提示されていたらしい(countdown()
関数は省略して main()
関数のみ挙げている)。
これだと for
ループの条件文で count
を直接参照しているので拙いよね,という指摘があったのだが,この指摘に対応しようとして失敗しているように見える。
おそらく main()
関数を修正するのであれば以下が妥当だと思う。
func main() {
mutex := sync.Mutex{}
count := 5
remaining := count
go countdown(&count, &mutex)
for remaining > 0 {
fmt.Println(remaining)
time.Sleep(500 * time.Millisecond)
mutex.Lock()
remaining = count
mutex.Unlock()
}
}
まず remaining
変数の初期化宣言を goroutine 起動前に行う。
さらに for
ループ内の fmt
.Println()
を remaining
変数を参照するよう変更する(こうすることで fmt
.Println()
をロックの外に出せる。なお,上のコードではカウント 0 を出力しないよう fmt
.Println()
関数の位置を変えている)。
といったところだろうか。
問題は最初の回答コードでも,その後の不完全修正コードでも,たぶん支障なく動いてしまうところなんだよね。 だってカウントダウンさせてる int 型の共有変数を参照してるだけだもん。 参照時に多少の不整合が起きても,おそらく見た目では分からない。
著者の方も指摘に対して
Thank you for spotting this. This is proof that race conditions are easy to miss!
GitHub Issue #2
と返しておられる通り,並行処理のデバッグがいかに難しいか分かる。
ただ,共有変数 count
へアクセスする際に「不変式が真2」であることをきちんと保証するためには,この実装をサボってはいけない。
Go に詳しい方なら「for
文で回さなくても channel を使えばいいし,なんなら atomic
パッケージも使えるぢゃん」と思うだろう(そしてそれは正しい)が『Go言語で学ぶ並行プログラミング』では4章でようやく sync
.Mutex
が登場したところで, channel は7章, atomic
パッケージは12章で登場する。
あしからず。
なお『Go言語で学ぶ並行プログラミング』本編には言及が見当たらないが sync
.Mutex
は再入不可である3。
参考図書
- Go言語で学ぶ並行プログラミング 他言語にも適用できる原則とベストプラクティス impress top gearシリーズ
- James Cutajar (著), 柴田 芳樹 (著)
- インプレス 2024-12-04 (Release 2024-12-04)
- Kindle版
- B0DNYMMBBQ (ASIN)
- 評価
読書会のために購入。インプレス社の本は Kindle 版より版元で PDF 版を買うのがオススメ。「並行処理」について原理的な解説から丁寧に書かれている。 Go で解説されているが Go 以外の言語でも応用できる。
- プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
- Alan A.A. Donovan (著), Brian W. Kernighan (著), 柴田 芳樹 (翻訳)
- 丸善出版 2016-06-20
- 単行本(ソフトカバー)
- 4621300253 (ASIN), 9784621300251 (EAN), 4621300253 (ISBN)
- 評価
著者のひとりは(あの「バイブル」とも呼ばれる)通称 “K&R” の K のほうである。この本は Go 言語の教科書と言ってもいいだろう。と思ったら絶版状態らしい(2025-01 現在)。復刊を望む!
- Go言語 100Tips ありがちなミスを把握し、実装を最適化する impress top gearシリーズ
- Teiva Harsanyi (著), 柴田 芳樹 (著)
- インプレス 2023-08-18 (Release 2023-08-18)
- Kindle版
- B0CFL1DK8Q (ASIN)
- 評価
版元で PDF 版を購入可能。事実上の Effective Go とも言える充実の内容。オリジナルは敢えてタイトルに “tips” という単語を入れるのを避けたのに邦題が「100 Tips」とかなっていて,原作者がお怒りとの噂(あくまで噂)
- Go言語による並行処理
- Katherine Cox-Buday (著), 山口 能迪 (翻訳)
- オライリージャパン 2018-10-26
- 単行本(ソフトカバー)
- 4873118468 (ASIN), 9784873118468 (EAN), 4873118468 (ISBN)
- 評価
-
𝕏 の TL はホンマにウザいので,エンジニアの方々は Bluesky に来て欲しい,マジで。 ↩︎
-
不変式の話は『プログラミング言語Go』に出てくる。また『Go言語で学ぶ並行プログラミング』の「訳者あとがき」でも『プログラミング言語Go』の内容を紹介する形で説明がある。でも『プログラミング言語Go』は絶版状態なんだよなぁ
orz
↩︎ -
再入云々については『Go言語で学ぶ並行プログラミング』の「訳者あとがき」に解説がある。たとえば Java のミューテックスは再入可能なため,その辺の差異について認識しておく必要がある。 ↩︎