Roman Roman - 3 months ago 10
C# Question

Threads synchronization. How exactly lock makes access to memory 'correct'?

First of all, I know that

lock{}
is synthetic sugar for
Monitor
class. (oh, syntactic sugar)

I was playing with simple multithreading problems and discovered that cannot totally understand how lockng some arbitrary WORD of memory secures whole other memory from being cached is registers/CPU cache etc. It's easier to use code samples to explain what I'm saying about:

for (int i = 0; i < 100 * 1000 * 1000; ++i) {
ms_Sum += 1;
}


In the end
ms_Sum
will contain
100000000
which is, of course, expected.

Now we age going to execute same cycle but on 2 different threads and with upper limit halved.

for (int i = 0; i < 50 * 1000 * 1000; ++i) {
ms_Sum += 1;
}


Because of no synchronization we get incorrect result - on my 4-core machine it is random number nearly
52 388 219
which is slightly larger than half from
100 000 000
. If we enclose
ms_Sum += 1;
in
lock {}
, we, of cause, would get absolutely correct result
100 000 000
. But what's interesting for me (truly saying I was expecting alike behavior) that adding
lock
before of after
ms_Sum += 1;
line makes answer almost correct:

for (int i = 0; i < 50 * 1000 * 1000; ++i) {
lock (ms_Lock) {}; // Note curly brackets

ms_Sum += 1;
}


For this case I usually get
ms_Sum = 99 999 920
, which is very close.

Question: why exactly
lock(ms_Lock) { ms_Counter += 1; }
makes program completely correct but
lock(ms_Lock) {}; ms_Counter += 1;
only almost correct; how locking arbitrary
ms_Lock
variable makes whole memory stable?

Thanks a lot!

P.S. Gone to read books about multithreading.

SIMILAR QUESTION(S)

How does the lock statement ensure intra processor synchronization?

Thread synchronization. Why exactly this lock isn't enough to synchronize threads

Answer

why exactly does lock(ms_Lock) { ms_Counter += 1; } make the program completely correct but lock(ms_Lock) {}; ms_Counter += 1; only almost correct?

Good question! The key to understanding this is that a lock does two things:

  • It causes any thread that contests the lock to pause until the lock can be taken
  • It causes a memory barrier, also sometimes called a "full fence"

I do not totally understand how lockng some arbitrary object prevents other memory from being cached in registers/CPU cache, etc

As you note, caching memory in registers or the CPU cache can cause odd things to happen in multithreaded code. (See my article on volatility for a gentle explanation of a related topic..) Briefly: if one thread makes a copy of a page of memory in the CPU cache before another thread changes that memory, and then the first thread does a read from the cache, then effectively the first thread has moved the read backwards in time. Similarly, writes to memory can appear to be moved forwards in time.

A memory barrier is like a fence in time that tells the CPU "do what you need to do to ensure that reads and writes that are moving around through time cannot move past the fence".

An interesting experiment would be to instead of an empty lock, put a call to Thread.MemoryBarrier() in there and see what happens. Do you get the same results or different ones? If you get the same result, then it is the memory barrier that is helping. If you do not, then the fact that the threads are being almost synchronized correctly is what is slowing them down enough to prevent most races.

My guess is that it is the latter: the empty locks are slowing the threads down enough that they are not spending most of their time in the code that has a race condition. Memory barriers are not typically necessary on strong memory model processors. (Are you on an x86 machine, or an Itanium, or what? x86 machines have a very strong memory model, Itaniums have a weak model that needs memory barriers.)

Comments