Lock-freeとWait-freeアルゴリズム
Lock-freeとWait-freeアルゴリズムとは、共有データにロックをかけて保護するアルゴリズムとは違い、複数のスレッドが同時並行的に、ある対象データを壊すことなしに読み書きすることを可能にするアルゴリズムである。Lock-free とはスレッドがロックしないことを意味しており、全てのステップにおいてシステムが必ず進行する。通常ロックを所有するスレッドは、コンテキストスイッチが発生した時点で、全体の進行を阻止することがあるので、Lock-free ではミューテックスやセマフォといった、排他制御のためのプリミティブが使われていないということになる。Wait-free とは、他のスレッドの動作に関係なく、スレッドがいかなる操作も有限のステップで操作を完了させられることを指す。Wait-free なアルゴリズムは Lock-free である。
意義
マルチスレッドプログラミングに置いて古典的な手法は、共有リソースにアクセスするときはロックをかけることである。ミューテックスやセマフォといった排他制御はソースコードのある領域( クリティカルセクション)を同時に実行しないようにし、それゆえ、共有メモリの構造を破壊しないようにする。もし、他のスレッドが事前に獲得したロックをあるスレッドが獲得しようとするときは、ロックが解放されるまでスレッドの動作は停止する。
スレッドが停止することは多くの理由で望ましくない。まず、スレッドがブロックされている間は、何も出来ない。そして、スレッドが優先順位の高い処理やリアルタイム処理を行っているならば、そのスレッドを停止することは望ましくない。また、複数のリソースにロックをかけることは、デッドロック、ライブロック、優先順位の逆転を起こすことがある。さらに、ロックを使うには、並列処理の機会を減らす粒度の荒い( = クリティカルセクションが長い )ロックを選択するか、バグを生みやすく注意して設計しないといけない粒度の細かいロックを選択するかというトレードオフ問題を生む。
Wait-freeなデータ構造
Wait-free のデータ構造を使ったプログラムを書くには、ミューテックスを使って書いてあるアルゴリズムを Wait-free のアルゴリズムに書き直すよりも、Wait-free のデータ構造の研究者が書いたスタック、キュー、セット、マップを使った方が望ましい。例えば、Java 5以降では、java.util.concurrentパッケージに Wait-free のデータ構造のクラスが入っている。Wait-free のデータ構造を使うことで、スレッド間で非同期にデータをやりとりするプログラムが書きやすくなる。
銀行預金の例
例えば、銀行口座への預金プログラムを作るとする。それぞれのスレッドをATMとする。預金預け入れをするには、現在の預金残高を読み出し、預入金額を足し算し、新しい預金残高を書き込むという処理が必要である。ロック方式のやり方の場合、1つ目のATMが預金をするとき、ほかのATMが同時に預金残高を変更しないよう、ロックをかける。さもないと、同時に処理してしまうと、最終的な預金残高に不整合が起きうる。この処理を Lock-free にするには、すべての預入要求を管理するスレッドを作り、そこに、Wait-free のキューを作り、ATMはそのキューに対して非同期にロックをかけることなく預入要求を入れ、預入要求を管理するスレッドはキューから順次取り出し、預金残高を更新する。このやり方の方が、わざわざ Lock-free の預金アルゴリズムを作るよりも、プログラミングは楽である。さらに、この手法は、キューがWait-freeであるので、Lock-free なだけでなく、Wait-freeでもある。預金残高の書き換え処理をn並列で行いたいなら、n個Wait-freeキューを作り、口座番号をnで割った余りでどのキューに入れるか決めるという方法で対応できる。
コンペア・アンド・スワップ
Lock-free や Wait-free アルゴリズムを作るには、CPUが専用のアトミックな命令を提供し、それを使う必要がある。最も重要なのは、コンペア・アンド・スワップ(Compare and swap, CASと省略する)である。Java では、java.util.concurrent.atomicパッケージ内のクラスに、compareAndSetメソッドとして存在する。これは、メモリアドレス、古い値、新しい値の3つを使う。もし、メモリアドレスに保存したある値が、指定された古い値ならば、それを新しい値に置き換え、そうでない場合は、何もしない。そして、この処理が成功したかどうかをプログラムに返す。CPUはこれをアトミックに実装する必要がある。現在のIntelプロセッサにはこの機能がある。この機能は、メモリーからデータを読み出し、変更し、書き戻すという処理を、他のスレッドがその間に同時に変更を行っていない場合にのみ行う、というアルゴリズムを可能にする。
例えば、先ほどの銀行口座の例で、別なアルゴリズムを見てみる。ATMは現在の値を読み出し、加算し、書き込む、という3ステップにおいて、3ステップ目をコンペア・アンド・スワップを使って行う。この3ステップの間に他のスレッドが値を書き換えなければ、3ステップ目のコンペア・アンド・スワップは成功する。しかし、この3ステップにおいて預金処理が同時並行で起これば、1ステップ目の読み出した金額と、3ステップ目の書き込みで「比較して交換」を使って読んだ金額が一致しないため、失敗し、ATMは1ステップ目からやり直す。全てのATMは成功するまでこの3ステップを繰り返す。このアルゴリズムは Lock-free ではあるが、Wait-free ではない。なぜなら、他のATMが預金することにより、自分のATMが何度も挑戦する必要があるからである。
Wait-Free Synchronization (1993) では、コンペア・アンド・スワップがなぜ、Wait-freeにおいて必要か証明している。
関連項目
外部リンク
- Atomic Ptr Plus Project lock-freeアルゴリズムに関する特許について
- http://www.audiomulch.com/~rossb/code/lockfree/