liveBook · Manning

Locking

As explained in Modern C, Third Edition

A mutex is a mutual-exclusion synchronization object that allows only one thread at a time to hold a protected critical section.

A thread updating a shared array cannot protect the whole array with _Atomic: C has no atomic array types. Instead, put the access inside a critical section protected by a mutex. In C, mtx_t is the mutex type supplied by <threads.h>. section 20.4 section 20.1

Protect a shared update

When an input thread adds a cluster of cells to a shared game board, it calls mtx_lock(&L->mtx), updates the board, then calls mtx_unlock(&L->mtx). Other input commands touch only atomic objects and do not need that critical section. section 20.4

mtx_lock waits until the calling thread can acquire the mutex; mtx_unlock releases it. Critical sections using the same mutex occur sequentially. Unlocking is a release operation, and successful locking is an acquire operation: a thread entering a protected section sees the effects of earlier sections protected by that mutex. TAKEAWAY: Use the same mutex for every critical section that relies on this protection. section 20.4 section 21.2

Choose the operation to match the wait:

  • mtx_lock waits to acquire the mutex; mtx_trylock tests whether it can be acquired without waiting; mtx_timedlock limits how long to wait. section 20.4
  • For work that depends on another thread, use a condition variable with the mutex. A condition wait releases the mutex and reacquires it before returning. Recheck the condition expression in a loop: returning does not establish that the expression changed. section 20.5 section 21.2
  • If a critical section needs multiple mutexes, acquire them in the same order across threads. Opposite acquisition orders can leave two threads each waiting for a mutex held by the other. section 20.7
Listing 20.2 The account thread function of B9
int account_thread(void* Lv) {
  life*restrict L = Lv;
  while (!L->finished) {
    // Blocks until there is work
    mtx_lock(&L->mtx);
    while (!L->finished && (L->accounted == L->iteration))
      life_wait(&L->acco, &L->mtx);

    // VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
    life_account(L);
    if ((L->last + repetition) < L->accounted) {
      L->finished = true;
    }
    // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    cnd_signal(&L->upda);
    mtx_unlock(&L->mtx);
  }
  return 0;
}

The accounting thread locks before checking whether work is ready, waits in a condition-testing loop, updates shared state, signals the update thread, and unlocks. section 20.5

Locking protects shared data, but it does not by itself guarantee that a threaded program will keep making progress. Deadlock and livelock need separate consideration. Timed condition waits are preferred here over untimed waits to avoid a thread remaining blocked indefinitely. section 20.7

How Modern C, Third Edition covers it

How deep it goes
Explains acquisition, release, visibility, condition waits, and deadlock-avoidance rules; defers extensive treatment of parallel-computing liveness.
Examples
Uses a shared game board, input and accounting threads, condition-wait loops, and a two-mutex deadlock scenario.
What it assumes
Assumes C structs and thread functions while defining mutex acquisition and critical sections as they arise.
What you can do after
Apply it via C <threads.h> mutex calls to protect shared data, coordinate condition waits, and order multiple locks.
How it connects
Connects critical sections with atomic objects, condition variables, release-acquire synchronization, deadlock, and livelock.

Also explained in

Compare how 18 books explain locking →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Modern C, Third Edition. The section links point to the parts of the book it draws on.