Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 719
Roman Avatar asked Aug 20 '11 13:08

Roman


1 Answers

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.)

like image 182
Eric Lippert Avatar answered Sep 24 '22 01:09

Eric Lippert