Let's say there is a class that contains a public int counter field that can be accessed by multiple threads, and this number will only increase or decrease.
When adding this field, which of the following schemes should be used, and why?
- lock(this.locker) this.counter++;
- Interlocked.Increment(ref this.counter);
- Change the access modifier of counter to public volatile
Worst (none of them actually work)
Change the access modifier of counter to public volatile
This approach is actually not secure at all, and the point about volatile is that multiple threads running on multiple CPUs buffer data and rearrange executed instructions.
If it is non-volatile, when CPU A increases by a value, CPU B needs to wait a while to see the increased value, which can lead to problems.
If it's volatile, it ensures that both CPUs see the same value at the same time. But it doesn't avoid cross-cutting read and write operations.
Adding value to a variable actually takes three steps
1. Reading, 2. Add 3. Write
Suppose thread A reads the value of counter as 1 and is not ready to increase, then thread B also reads the value of counter as 1, and then both threads start to perform incremental and write operations. The value of the final counter is 2. This is not right, both threads have done an increase operation, and the correct result should be 3. So labeling it as volatile is simply unsafe.
It's better
lock(this.locker) this.counter++;
This way it's safe (remember to lock everywhere you want to access this.counter, of course). It prevents any other thread from executing the locked code. And it also prevents the multi-CPU instruction sequencing problem mentioned above. The problem is that lock is slow in performance, and if you use lock in other unrelated places, it can block your other threads.
Best
Interlocked.Increment(ref this.counter);
This is safe and very efficient. It performs read, increase, write three operations in one atom without being interrupted in the middle. Because it doesn't affect other code, you don't need to remember locks elsewhere. And it's also very fast (as MSDN says, on today's CPUs, it's often just an instruction).
But I'm not entirely sure if it can also solve the CPU's instruction ordering problem, or if it needs to be used in conjunction with volatile and this increment.
Supplement: What problems does volatile solve well?
Since volatile can't prevent multithreading, what can it do? A good example is that you have two threads, one always writes to a variable, let's say this variable is queneLength, and the other always reads data from this variable. If queueLenght is not volatile, thread A may read 5 times, but thread B may see delayed data, or even data in the wrong order. One solution is to use lock, but in this case you can also use volatile. This ensures that thread B always sees the latest data written by thread A, but this logic only works if you don't read it when you write it, and if you don't write it when you read it. Once multiple threads want to do read-modify-write operations, you need to use Interlocked or lock.
|