c++multithreadingconcurrencysynchronizationcontext-switching

Does the context switch retain (keep state of variables) the value of the variables when it resumes the task?


Pseudocode (Assuming that there is no synchronization of any kind):

count = 0; // Global

Thread 1:

count = count + 1;

suppose that after reading count "captured" the 0 and context switch occurred. It went to another thread and the following happened:

Thread 2:

count = count + 1; // Count = 1. Finished all without context switching

Decides to return to thread 1:

after finishing thread 1, will count be worth 1 because it read 0 when it swapped, or will it be worth 2? That is,

does the context switch retain the value of the variables when it resumes the task?


Solution

  • To actually answer your question... The following can (and will!) indeed happen:

    Thread 1: Load "count" into CPU register "A". Result: A = 0
    Thread 1: Increment CPU register A. Result: A = 1
    
    (Context switch)
    
    Thread 2: Load "count" -> 0
    Thread 2: Increment -> 1
    Thread 2: Store 1 in "count"
    
    (Context switch)
    
    Thread 1: Store 1 from register A in "count"
    

    Afterwards, count is still 1, and one increment has been "lost". In practice, pretty much anything can happen, including the counter suddenly going backwards or reading totally nonsensical values.

    What's preserved across a context switch is the CPU's registers. In order to operate on a variable, the CPU first has to load the variable from memory into a register. Then it can operate on the copy of the variable in its register. When it's done, it'll write the value from the register back into memory (potentially immediately after the increment, or potentially much later - it's unspecified). If another thread changes the variable in the meanwhile, the value in the CPU's register will be outdated and your program breaks.

    There are so-called atomic variables (i.e. std::atomic<int>) that allow "read-modify-write" operations that can't be interrupted by a context switch (and can also be done concurrently on multiple different CPU cores).

    On some CPU architectures (ones without coherent caches), data written to memory by one thread isn't even visible to other threads until a so-called release operation has been executed that makes this data available to other threads. This means that, if there isn't proper synchronization, different threads could read totally different values from a variable even when all write accesses to it have already completed. In general, "release" operations make data available to threads that perform an "acquire" operation. These operations could be the acquiring and releasing of a mutex, but it could also be an access to an atomic variable with suitable semantics (these translate to special machine instructions that not only access the variable but also control cache coherence).

    Note that acquire/release semantics are also barriers for the compiler: In the absence of barriers (and atomics), the compiler is free to re-order memory accesses as long as the program executes "as-if" it was unmodified, not taking threads into account. This means that a compiler can re-order, omit, and duplicate memory reads and writes as it pleases. A release barrier (i.e. mutex release) prevents the compiler from moving accesses "down" after the barrier, so that the barrier makes previous writes available to other threads. An acquire barrier (i.e. mutex acquire) prevents the compiler from moving accesses "up" before the barrier, so that the barrier makes other threads' writes visible to the thread that executed the barrier. When a release on one thread matches up with an acquire on another (i.e. they both use the same mutex or the same atomic variable), data written by the releasing thread becomes visible to the acquiring thread, and you can transfer data between threads safely without everything blowing up.

    For more info on all of this atomics / lock-free stuff, I'd recommend you to watch this presentation by Herb Sutter at CppCon 2014:

    Part 1: https://www.youtube.com/watch?v=c1gO9aB9nbs

    Part 2: https://www.youtube.com/watch?v=CmxkPChOcvw