n++
and操作は、n--
アトミックであるとは限りません。各操作には次の 3 つのフェーズがあります。
- メモリから現在の値を読み取る
- 値の変更 (インクリメント/デクリメント)
- 値をメモリに書き込む
両方のスレッドがこれを繰り返し実行しており、スレッドのスケジューリングを制御できないため、次のような状況が発生します。
- スレッド 1: 取得
n
(値 = 0)
- スレッド 1: インクリメント (値 = 1)
- スレッド 2: 取得
n
(値 = 0)
- スレッド 1: 書き込み
n
(n == 1)
- Thread2: デクリメント (値 = -1)
- スレッド 1: 取得
n
(値 = 1)
- スレッド 2: 書き込み
n
(n == -1)
等々。
これが、共有データへのアクセスをロックすることが常に重要である理由です。
-- コード:
static void Main(string[] args)
{
int n = 0;
object lck = new object();
var up = new Thread(() =>
{
for (int i = 0; i < 1000000; i++)
{
lock (lck)
n++;
}
});
up.Start();
for (int i = 0; i < 1000000; i++)
{
lock (lck)
n--;
}
up.Join();
Console.WriteLine(n);
Console.ReadLine();
}
-- 編集:lock
仕組みの詳細...
ステートメントを使用すると、指定したオブジェクト (上記のコードlock
のオブジェクト) のロックを取得しようとします。lck
そのオブジェクトが既にロックされている場合、このlock
ステートメントにより、コードは続行する前にロックが解除されるまで待機します。
C#ステートメントは実質的にCritical Sectionlock
と同じです。実際には、次の C++ コードに似ています。
// declare and initialize the critical section (analog to 'object lck' in code above)
CRITICAL_SECTION lck;
InitializeCriticalSection(&lck);
// Lock critical section (same as 'lock (lck) { ...code... }')
EnterCriticalSection(&lck);
__try
{
// '...code...' goes here
n++;
}
__finally
{
LeaveCriticalSection(&lck);
}
C#lock
ステートメントでは、そのほとんどが抽象化されています。つまり、クリティカル セクションに入って (ロックを取得して)、そこから出るのを忘れるということがずっと難しくなっています。
ただし重要なことは、ロックしているオブジェクトのみが影響を受け、同じオブジェクトのロックを取得しようとしている他のスレッドに関してのみ影響を受けるということです。コードを記述してロック オブジェクト自体を変更したり、他のオブジェクトにアクセスしたりすることを妨げるものは何もありません。 あなたは、あなたのコードがロックを尊重し、共有オブジェクトへの書き込み時に常にロックを取得することを確認する責任があります。
そうしないと、このコードで見たような非決定論的な結果、または仕様作成者が「未定義の動作」と呼びたいものになります。Here Be Dragons (無限のトラブルが発生するバグの形で)。