マルチスレッドプログラミングの基本

マルチスレッドプログラミングは直感とかなり異なる挙動をすることが多く、非常に困難なプログラミングであることが知られている。不具合も発見しにくく、高負荷になった時にのみ再現したり極めて稀な状態になった時にのみ再現したり等、解決が困難なだけでなく不具合の発見や再現すら困難であることもある。不具合を発見してもその現象は不可解であることが多く、現象から直感で原因を見つける事は難しい。

つまり、マルチスレッドプログラミングはシングルスレッドプログラミングに比べてはるかに困難で厄介者なのだ。
この記事に、マルチスレッドプログラミングの困難さを少しでも回避するためのルールやお作法を載せておくことにする。

よくある不具合

デッドロック
マルチスレッドプログラミングで最もポピュラーな不具合。
二人以上のユーザー(≒スレッド)がお互いのリソースのロックが解放されるのを待ち続けること。
一般的にはロックを取る順序を制御し、デッドロックが起こりえないようにすることで解決する。
デッドロックは不具合が発見しやすいのと割と解決しやすいのが良い所。


レースコンディション
直訳すると競合状態。複数スレッドの処理のタイミングによって共有メモリの状態が不正な状態になること。
マルチスレッドプログラミングではしばしば発生し、原因の特定・解決が困難。開発環境では再現せず本番環境でのみ再現するようなこともよくある厄介者。
正しく排他制御されてないことが原因。


スタベーション
ロック獲得までの解放待ちの時間が長すぎて処理が進まない不具合。特定のリソースを長時間専有してしまうことが原因。
ロックの粒度を下げたりロックストライピングをしたり等でロックを奪い合う頻度を下げることで解決する。

プログラミング作法

フレームワークに頼る
上で何度も述べたように、マルチスレッドプログラミングは困難であり、自分で気づかぬうちに不具合を仕込んでしまうことが良くある。これは上級プログラマでも同様だ。これを回避するにはマルチスレッドなプログラムを書かないことがベストだが、それが無理な場合はスレッドの管理を自分で行わず、既存のフレームワークに任せること。広く使われているフレームワークであればあるほど、不具合が仕込まれている可能性は低い。かつプログラマにとってシンプルに実装出来ることも多い。

Java プログラマであれば java.util.concurrent パッケージの中身をひと通り眺めておくこと。ここには汎用性が高く、安全で、パフォーマンスも良い優れたライブラリが揃っている。自前で Semaphore の実装など行なってはならない。必ず既存のフレームワーク、ライブラリを利用すること。


目的のライブラリが探しても見つからない場合、もう一度探す事。
それでも無ければ設計を見なおして、既存ライブラリを利用したシンプルな実装を目指すこと。
シンプルな実装にならなければ実装しないこと。


シンプルな実装にすることを心がける
上記のお作法とほぼ同義。マルチスレッドプログラミングで複雑な設計は極めて悪であることを理解すること。その設計で、どのような状況でロックやバリアを行うべきか説明出来なければ実装してはならない。

共有するリソースは極限まで減らすこと。出来れば 0 か 1 を目指す。複数のリソースを適切にロック獲得・解放するプログラムを書くことを目指すなら、まずリソースの数を減らすことを目指すこと。複数ロックする場合は、デッドロックを防ぐためロックする順序を規約で決めておくこと。


適切に同期する
「ここで排他制御すべきかどうかわからない。困ったら synchronized」はやってはいけない。適切な設計をして、同期する箇所を明らかにしておくこと。排他制御すべきかどうかわからないような状態に陥ったらそれまでのコードを捨てること。場合によっては設計も捨てること。

マルチスレッドプログラミングに関わらずプログラミング全般の決まりごととして、自分が理解できないコードを書いてはならない。
このルールはマルチスレッドプログラミングでは極めて重要だ。誰もデバッグができなくなり、作りなおすことになる。
決して自分が理解できないコードを書いてはならない。


過剰な同期は避ける
スタベーションを起こすので過剰な同期はしてはならない。

また、過剰に同期をすると不具合発生時に原因の特定が困難になる。同期は必要最小限に止めること。逆に言えば、同期された領域内では最小限の処理しか行わないこと。


性能を測定出来るようになる
マルチスレッドプログラミングとパフォーマンスは強い関係がある。性能が問題で無いなら、シングルスレッドで安全に、不正な状態が起こりえないプログラムを書くべきだ。それが無理ならマルチスレッドプログラミングに手を出すことになるが、プログラムを書くと同時に性能を測定出来るようになること。例えば Linux の基本的な性能測定ツールは使えるようになっておくこと。

マルチスレッドだからといって、動作環境の性能を向上させればプログラムの性能もリニアに向上するわけではない。むしろ場合によっては性能が劣化することさえある。書籍「Unix システムパフォーマンスチューニング」ではプロセッサを1つから2つに増設したところ、性能が30%程度劣化した例が紹介されている。


つまりマルチスレッドとパフォーマンスは気難しい関係であり、コードや環境に変更を加えるたびに測定する必要がある。ほんの数行加えたロックで性能がひどく劣化したり、コンテキストスイッチのコストが増大したりなど劣化の要因は様々だ。

並行性を上げることで解決できる類のものなら、出来れば事前の設計段階で Lock-free,Wait-free アルゴリズムを使うことができないか検討すること。


ドキュメント化する
マルチスレッドなプログラムを書いたら、設計をドキュメント化しておくこと。
また、そのプログラムの部品群について、スレッドセーフならスレッドの安全性と共にスレッドセーフであると明記しておくこと。

スレッドの安全性は下記の5つ。

  • 不変

値は状態を持たないので本質的にスレッドセーフなもの。

  • 無条件スレッドセーフ

値は状態を持つが、どのように使用されてもスレッドセーフなもの。

  • 条件付きスレッドセーフ

値は状態を持ち、一部スレッドセーフであるが、スレッドセーフでない部分も存在するもの。
スレッドセーフに使うにはユーザーが適切に同期を行う必要がある。

  • スレッドセーフではない

複数のスレッドからアクセスされることを想定していないもの。

  • スレッド敵対

全てのメソッドを同期的に使用してもスレッドセーフにならないもの。

まとめ

シングルスレッドプログラミングとマルチスレッドプログラミングは同じ言語であってもかなり異質なものである。