再入可能なロックの話
先週、@t_wadaさんと@yosuke_furukawaさんと議論した再入可能性に関する私の経験について書きました | ロック(ミューテックス)の再入可能性 https://t.co/j3xeOUxaWt
— Yoshiki Shibata/柴田芳樹 (@yoshiki_shibata) October 22, 2018
突然のロックの話
いきなりロックの話をしましたが、10月に(なぜか)一緒に働いてるメンバーとの中で大盛り上がりした話題です。もともとはリクルートテクノロジーズで行われている、柴田芳樹さんのプログラミングGo勉強会で話題になった話です。
ここにも書いてあるのですが、 Golang では sync.Mutex
を使ったロックでは再入可能ではありません。
一方 Java のロックは再入可能です。
で、この設計に関しては合理的な解説が Russ Cox さんからされています。
意訳すると以下のような感じですね。
再入可能なロックはbad ideaだ。 mutex を利用する主な理由は mutex が不変式を保護するためだ。 不変式というのは、 例えば 円環(linkedlistのようなもの) の 全要素 p に対して p.Prev.Next == p が成立するといった内部の不変式であったり、 自分のローカル変数 x は p.Prev と等しい、といった外部の不変式を指している。 mutexのロックを取るというのは、「私は不変式を維持する必要がある」というのと、「これらの不変式を一時的に壊す」という二点の表明である。 また、 mutexのロックを開放するというのは、「不変式にはもう依存していない」というのと、「一時的に壊した不変式は元に戻っている」という表明である。 再入可能なロックを取るというのは、いったん mutex のロックを確保した状態で再び mutex のロックを取るが、この状態では不変式を一時的に壊している可能性がある。再入可能なロックは不変式を保護しない。 再入可能なロックというのは単なる間違いであり、バグの温床になる可能性がある。
この話には納得できます。ロックの再入可能可否についてはできないようにする方が良いというのは納得できます。余談ですが、プログラミング言語Go以外の本にもEffective Javaにも同様の話がありますし、詳解Unixプログラミングでもこの手の話はあります。
じゃあなぜJavaは再入可能なのか
Javaは既存のAPI資産がスレッドセーフになっているものがあり、それらの中にはメソッドの中でロックを取っているものもあります。この状況では再入可能なロックを認めざるを得なかったため、今ではバッドプラクティスとして残っています。例を挙げると、HashTableやVectorといった既存のコレクションクラスがスレッドセーフですね。
ちなみに 詳解UNIXプログラミングの中に出てくる pthread も同じく再入可能にするオプションがあります。これも既存のAPIでスレッドセーフなものが多いから渋々追加している機能です。
というわけで、そもそも再入可能なロックを提供すること自体はGoでもJavaでもUnixプログラミングでも望まれていない訳ですね。
社内で盛り上がった内容
さて、再入可能なロックを提供しないという方針については基本的に全員同意しており、これまでの話に対しては反対意見は無いですが、よくよく考えると少し腑に落ちない点があります。それは、「不変式を守るのは、ロックの有無に限らず守らないといけないのではないか?」という点です。
この不変式を守るという話でmutexの再入可能なロックを取らなくしただけではなく、本来的には壊さないように全員が気をつけるべきであり、この問題について取り組んだのが、有名な「契約による設計 (Design By Contract)」の話です。
EiffelやD言語ではこのDesign By Contract を言語仕様に取り入れているというのは有名な話ですね。
Golangでは、不変式を守るという話についてここまで説明があるにもかかわらず、Design By Contractを言語仕様には取り入れていませんし、assertすらありません。
assertを提供していない件についてはちゃんとFAQがありますが、契約による設計を取り入れてない理由も含めてGo言語がそういう取捨選択をしている理由はなんなのか、というので、話が盛り上がり、何度か柴田さんやkoichikさん、和田さんを含めて議論しました。
議論ポイントを整理すると、「ロックを再入可能にしない」という設計については納得するものの、「そこまで不変式を守ると言うなら契約による設計やassertについては入れないという選択をしたのはどうしてか」といった部分ですね。
ただどちらの主張も実は衝突するような話ではなく、「ロックを再入可能にしないのは不変式を壊したくないから、ただし、不変式を壊したくないからと行ってDesign By Contractまでのゴツい仕様は入れたくなかった」という話だろうと、古川は推測していますし、この理由である程度納得しています。
並列並行処理
色んなプログラミング言語を知っていると正解は一つではないというか多様な正解があるという事がわかります。Goの考え方はUnix哲学的なものであり、シンプルな解答を用意しつつも、バグの温床になるような言語仕様は避けようとする考え方が見られます。
一方で、この手のmutexの問題は基本的に「並列プログラミングが難しい」という問題に根ざしたものであり、これに対して何度も挑戦していないと議論ができるようなポイントに到達できないな、と改めて感じ、年末年始はこの本でも読もうかなと思いました。