これは、データ競合の典型的な例です。
それでは、add()の機能を詳しく見てみましょう。
add()
{
x = x + 1;
}
これは次のように解釈されます。
- Xの最新の値を教えて、プライベートワークスペースに保存します
- プライベートワークスペースに保存されている値に1を追加します
- ワークスペースにあるものを、コピー元のメモリ(グローバルにアクセス可能)にコピーします。
これをさらに説明する前に、コンテキストスイッチと呼ばれるものがあります。これは、オペレーティングシステムがプロセッサの時間をさまざまなスレッドやプロセスに分割するプロセスです。このプロセスは通常、スレッドに有限のプロセッサ時間を与え(Windowsでは約40ミリ秒)、その作業を中断し、プロセッサがレジスタに持っているすべてのものをコピーして(したがって、その状態を保持して)、次のタスクに切り替えます。これは、ラウンドロビンタスクスケジューリングと呼ばれます。
処理が中断されて別のスレッドに転送されるタイミングを制御することはできません。
ここで、同じことを行う2つのスレッドがあると想像してください。
1. Give me the most recent value of X and store it in my private workspace
2. Add 1 to that value that is stored in my private workspace
3. Copy what I have in my workspace to the memory that I copied from (that is globally accessible).
そして、それらのいずれかが実行される前に、Xは1に等しくなります。
最初のスレッドは最初の命令を実行し、そのプライベートワークスペースに作業中の最新のXの値を格納する場合があります-1。次に、コンテキストスイッチが発生し、オペレーティングシステムがスレッドに割り込んで、キュー内の次のタスク、それはたまたま2番目のスレッドです。2番目のスレッドも1に等しいXの値を読み取ります。
スレッド番号2は、なんとか完了まで実行されます。「ダウンロード」した値に1を加算し、計算値を「アップロード」します。
オペレーティングシステムは、コンテキストスイッチを再度強制します。
これで、最初のスレッドは中断された時点で実行を継続します。それでも、最新の値は1であると見なされ、その値が1ずつインクリメントされ、計算結果がそのメモリ領域に保存されます。そして、これがデータの競合が発生する方法です。最終結果は3になると予想しますが、2です。
この問題を回避するには、ロック/ミューテックス、コンペアアンドスワップ、アトミック操作など、さまざまな方法があります。