n++ and n-- not guaranteed to be atomic. Each operation has three phases:
- Read current value from memory
- Change value (increment / decrement)
- Write value to memory
Since both of your threads do this several times, and you do not control thread scheduling, you will have situations like this:
- Thread1: Get
n (value = 0) - Thread1: Increment (value = 1)
- Thread2: Get
n (value = 0) - Thread1: Write
n (n == 1) - Thread2: Decrement (value = -1)
- Thread1: Get
n (value = 1) - Thread2: Write
n (n == -1)
And so on.
This is why it is important to always block access to shared data.
- Code:
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(); }
- Edit: more about how lock works ...
When you use the lock statement, it tries to get the lock on the object you supply it with - the lck object in my code above. If this object is already locked, the lock statement will cause your code to wait until the lock is released before continuing.
The C # lock statement actually matches the Critical section . In fact, it looks like the following C ++ code:
// 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); }
The C # lock operator abstracts most of this, which means that itβs much harder for us to enter the critical section (get the lock) and forget to leave it.
The important thing is that only your lock object is affected, and only with respect to other threads trying to get a lock on the same object. Nothing prevents you from writing code to modify the lock object itself or access any other object. YOU are responsible for ensuring that your code matches locks and always acquires a lock when writing to a shared object.
Otherwise, you will have a non-deterministic result, as you saw with this code, or what speculators call "undefined behavior". Here is the Be Dragons (in the form of errors that you will encounter endlessly).