liveBook · Manning

Locking granularity

As explained in C++ Concurrency in Action, Second Edition

Lock granularity is the amount of data protected by a single lock.

Lock granularity describes how much data one lock protects. A fine-grained lock protects a small amount; a coarse-grained lock protects more. Choose a scope large enough to protect a complete operation, but avoid making unrelated work wait for the same mutex. The amount of data protected and the time a lock is held are separate decisions. section 3.2.8

A stack operation split across calls

Suppose one thread checks `empty()`, reads `top()`, then calls `pop()`. Locking each call separately does not protect that three-call operation: another thread can change the stack between calls. A thread-safe interface can instead combine retrieval and removal in one `pop()` operation, with the mutex held throughout. Its copy constructor must likewise hold the source mutex across the copy. section 3.2.3

Table 3.1. A possible ordering of operations on a stack from two threads (view table figure)

Thread A

Thread B

if(!s.empty())    
    if(!s.empty())
  int const value=s.top();    
      int const value=s.top();
  s.pop();    
  do_something(value);   s.pop();
      do_something(value);

The two-thread ordering makes the gap between individually protected stack calls visible. section 3.2.3

Making one mutex protect everything avoids those gaps only by creating another problem: threads accessing different data must still take turns. An early multiprocessor Linux kernel used a single global kernel lock; later, finer-grained locking reduced contention. Finer-grained designs can, however, require more than one mutex to protect an operation. section 3.2.3

Keep processing outside the lock

Lock while obtaining shared data, unlock to process the local data, then lock again to write the result. `std::unique_lock` supports this explicit `unlock()` and `lock()` sequence. Avoid time-consuming file I/O while holding a mutex unless that mutex protects access to the file: waiting threads otherwise remain blocked for no useful reason. section 3.2.8

void get_and_process_data()
{
    std::unique_lock<std::mutex> my_lock(the_mutex);
    some_class data_to_process=get_next_data_chunk();
    my_lock.unlock();                                   #1
    result_type result=process(data_to_process);
    my_lock.lock();                                     #2
    write_result(data_to_process,result);
}

The function marks the two shared-data accesses and the processing performed between them without the lock. section 3.2.8

For a comparison of two objects, one option is to lock each object's mutex only long enough to read its value, then compare the copied values. This holds one mutex at a time; the reads are not one simultaneously locked operation. Decide whether that behavior protects the operation you actually need. section 3.2.8

How C++ Concurrency in Action, Second Edition covers it

How deep it goes
Defines fine and coarse granularity, then works through stack races, contention, and shortening critical sections.
Examples
Uses interleaved stack calls, a global kernel lock, and `std::unique_lock` around data processing.
What it assumes
Uses C++ stack operations and mutex guards while explaining the concurrency risks those operations create.
What you can do after
Diagnose/debug with it by spotting operations split across locks and work that holds a mutex unnecessarily.
How it connects
Links lock granularity to mutexes, race conditions, interface design, contention, and critical sections.

Related concepts

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