Locking and Atomics

Mutex

Exclusive lock

  • Can be taken by only one thread

  • Methods:

    • lock: take (and possibly wait for) lock

    • unlock

    • try lock: take lock, or return error if locked

#include <mutex>
std::mutex lock;

lock.lock();
... critical section ...
lock.unlock();

Scoped Locking (1)

What if a critical section throws?

lock.lock();
do_something_errorprone(); // possibly throws
do_more_of_it(); // possibly throws
lock.unlock();
  • Lock remains locked

  • ⟶ Deadlock

Scoped Locking (2)

Deterministic destructors

  • Objects are destroyed at end of block

  • Unlike Java, Python, … (garbage collection)

  • Exception safety!

std::lock_guard
...
// critical section
{
  std::lock_guard<std::mutex> g(lock); // lock.lock()
  do_something_errorprone();
  do_more_of_it();
  // ~guard does lock.unlock();
}
...

Mutex: Pros and Cons

Mutexes are expensive

  • Context switch on wait ⟶ expensive

  • Can only be used in thread context

  • Interrupts cannot wait

  • Never share mutexed objects with an interrupt routine!

  • Undefined behavior

Mutexes are easy

  • Can protect arbitrarily long critical sections

Atomic Instructions (1)

Simple integers don’t need a mutex ⟶ atomic instructions

GCC: atomic built-ins
static int global;
void inc() {
  __sync_fetch_and_add(&global, 1);
}
Windows
static LONG global;
void inc() {
  InterlockedIncrement(&global);
}

Atomic Instructions (2)

#include <atomic>
std::atomic<int> global(0);
void inc() {
  global++;
}
  • Specializations for all types that are capable

Self-Deadlocks (1)

Deadlocks: one more dimension in bug-space

  • Usually between two threads

  • Self-deadlock: between one thread

The most obvious self-deadlock
std::mutex lock;
...
lock.lock();
lock.lock(); // wait forever

Self-Deadlocks (2)

(Only slightly) more intelligent ways to lock the same mutex twice …

  • Calling a callback while holding the lock

    • What?

    • Passing control to untrusted code when critical??

  • Public method uses another public method of the same object

    • ⟶ Safer: distinguish between “locked” (public) and “unlocked” (private) methods

    • “locked” may only use “unlocked”

⟶ Design decision

Working Around Self-Deadlocks: Recursive Mutex

Recursive mutex …

  • Same thread can enter an arbitrary number of times

  • Has to exit exactly as many times to release the mutex for other threads

The most obvious self-deadlock
std::recursive_mutex lock;
...
lock.lock(); // locked for others
lock.lock(); // granted
// ...
lock.unlock();
lock.unlock(); // released for others