Search code examples
cmultithreadingcomputer-sciencesemaphoremutual-exclusion

What prevents a race condition when checking a semaphore value?


I'm studying multithreading and trying to understand the concept of semaphores and mutual exclusion. Most of the examples I find online use some sort of library (e.g. pthread) to implement the semaphore or mutex, but I'm more interested in the implementation of a simple semaphore that establishes a critical section -- no more than one thread accessing a particular region of memory.

For this task, I believe I would need a mutex (a.k.a. a binary semaphore if I understand the terminology correctly). I can see how the semaphore would prevent a race condition by "locking" the section of code to a single thread, but what prevents a race condition from occurring at the semaphore itself?

I imagine a binary semaphore to hold an int value to keep track of the lock:

Semaphore
---------
int lock = 1;

unsigned P(void){
    if(lock > 0){
        lock--;
        return 0; /* success */
    }
    return 1; /* fail */
}

void V(void){
    lock++;
}

Suppose two threads call the P function at the same time, they both reach the if(lock > 0) check at the same time and evaluate the condition as true -- this creates a race condition where both threads are granted access to the same region of memory at the same time.

So what prevents this race condition from occurring in real world implementations of semaphores?


Solution

  • Locking and relasing semaphores and/or mutexes happen as atomic operations, this means the CPU cannot be withdrawn from the current process. This ensures, that as soon as a mutex-lock is started (it consists of either a single or a few CPU-instruction (microcode)), the process keeps the CPU until the locking/releasing is done.
    There are also different ways to implement threading, which can either be a direct support by CPU (kernel-space) or through a library (such as pthreads) in user-space.


    From OSDev.org

    An atomic operation is an operation that will always be executed without any other process being able to read or change state that is read or changed during the operation. It is effectively executed as a single step, and is an important quality in a number of algorithms that deal with multiple indepent processes, both in synchronization and algorithms that update shared data without requiring synchronization.


    Here is a nice article on atomicity, too (although in Delphi).