Mutex

Join provides thread synchronization primitives built on top of POSIX threads. These mutexes enable safe concurrent access to shared resources across threads and processes.

Mutex types:

  • Mutex — basic mutual exclusion lock
  • RecursiveMutex — allows recursive locking by the same thread
  • SharedMutex — inter-process synchronization via shared memory
  • ScopedLock — RAII-based automatic lock management

Mutex types

Mutex

Basic mutex for thread synchronization:

// libjoin.
#include <join/mutex.hpp>

using namespace join;

Mutex mutex;

mutex.lock();
// Critical section
mutex.unlock();

Characteristics:

  • Non-recursive — deadlock if same thread locks twice
  • Thread-local — only for threads within the same process
  • Lightweight — minimal overhead

RecursiveMutex

Allows the same thread to acquire the lock multiple times:

RecursiveMutex mutex;

void recursiveFunction(int depth)
{
    mutex.lock();

    if (depth > 0)
    {
        recursiveFunction(depth - 1);  // OK: recursive lock
    }

    mutex.unlock();
}

Characteristics:

  • Recursive — same thread can lock multiple times
  • Balanced unlocks — must call unlock() same number of times as lock()
  • Slightly higher overhead than basic Mutex

SharedMutex

Synchronizes access across different processes via shared memory:

// libjoin.
#include <join/memory.hpp>
#include <join/mutex.hpp>

// C++.
#include <new>

using namespace join;

// Process A: create the shared memory segment and construct the mutex in it
ShmMem shm(sizeof(SharedMutex), "/my_mutex");
SharedMutex* mutex = new (shm.get()) SharedMutex();

mutex->lock();
// Critical section
mutex->unlock();
// Process B: open the same segment and use the existing mutex
ShmMem shm(sizeof(SharedMutex), "/my_mutex");
SharedMutex* mutex = static_cast<SharedMutex*>(shm.get());

mutex->lock();
// Critical section
mutex->unlock();

Characteristics:

  • Process-shared — works across process boundaries
  • Robust — automatically handles owner death with PTHREAD_MUTEX_ROBUST
  • Placement new — must be constructed in shared memory with placement new
  • Explicit destruction — call destructor manually before unlinking shared memory

Important: SharedMutex must be allocated in shared memory using placement new to be visible across processes.


Lock operations

Blocking lock

mutex.lock();  // Blocks until lock is acquired

Non-blocking lock

if (mutex.tryLock())
{
    // Lock acquired
    mutex.unlock();
}
else
{
    // Lock not available
}

Unlock

mutex.unlock();  // Release the lock

ScopedLock (RAII)

Automatically acquires and releases a lock using RAII:

void criticalSection()
{
    Mutex mutex;

    ScopedLock<Mutex> lock(mutex);
    // Lock acquired automatically

    // Critical section
    // ...

    // Lock released automatically when lock goes out of scope
}

Exception safety

ScopedLock ensures the mutex is unlocked even if an exception is thrown:

Mutex mutex;

try
{
    ScopedLock<Mutex> lock(mutex);

    if (errorCondition)
    {
        throw std::runtime_error("Error");
    }

    // Lock automatically released here
}
catch (...)
{
    // Mutex already unlocked by ScopedLock destructor
}

Works with all mutex types

RecursiveMutex recMutex;
ScopedLock<RecursiveMutex> lock1(recMutex);

SharedMutex* sharedMutex = /* from shared memory */;
ScopedLock<SharedMutex> lock2(*sharedMutex);

Usage patterns

Basic critical section

Mutex mutex;
int sharedCounter = 0;

void increment()
{
    mutex.lock();
    ++sharedCounter;
    mutex.unlock();
}

RAII-based critical section

Mutex mutex;
int sharedCounter = 0;

void increment()
{
    ScopedLock<Mutex> lock(mutex);
    ++sharedCounter;
    // Automatic unlock
}

Conditional locking

Mutex mutex;

void tryUpdate()
{
    if (mutex.tryLock())
    {
        // Update data
        mutex.unlock();
    }
    else
    {
        // Skip update, mutex busy
    }
}

Recursive locking

RecursiveMutex mutex;
std::vector<int> data;

void processElement(int index)
{
    ScopedLock<RecursiveMutex> lock(mutex);

    if (index < data.size())
    {
        // Process current element

        if (needsRecursion)
        {
            processElement(index + 1);  // OK: recursive lock
        }
    }
}

Inter-process synchronization

ShmMem creates the segment, or opens it if it already exists, and throws std::system_error on failure. The segment is unmapped when ShmMem is destroyed, but it is only removed by ShmMem::unlink().

// libjoin.
#include <join/memory.hpp>
#include <join/mutex.hpp>

// C++.
#include <iostream>
#include <new>

using namespace join;

try
{
    // creates the segment, or opens it if another process already did
    ShmMem shm(sizeof(SharedMutex), "/my_mutex");

    // Process A constructs the mutex (placement new)...
    SharedMutex* mutex = new (shm.get()) SharedMutex();

    // ...Process B reuses it instead (no construction needed):
    // SharedMutex* mutex = static_cast<SharedMutex*>(shm.get());

    mutex->lock();
    // Critical section
    mutex->unlock();
}
catch (const std::system_error& e)
{
    std::cerr << "shared memory error: " << e.what() << "\n";
}

// Cleanup (once all processes are done)
ShmMem::unlink("/my_mutex");

Native handle

Access the underlying pthread_mutex_t for advanced operations:

Mutex mutex;
pthread_mutex_t* handle = mutex.handle();

// Use with pthread functions
pthread_cond_wait(&cond, handle);

Best practices

  • Use ScopedLock — automatic unlock prevents deadlocks from early returns or exceptions
  • Minimize critical sections — keep locked regions as short as possible
  • Avoid nested locks — can lead to deadlocks unless using RecursiveMutex
  • Use tryLock() carefully — ensure proper error handling when lock fails
  • SharedMutex placement — always allocate in true shared memory regions
  • Consistent locking order — when using multiple mutexes, always acquire in the same order

Common pitfalls

Forgetting to unlock

// BAD: exception causes unlock to be skipped
mutex.lock();
if (error)
{
    throw std::runtime_error("Error");  // mutex never unlocked!
}
mutex.unlock();

// GOOD: use ScopedLock
ScopedLock<Mutex> lock(mutex);
if (error)
{
    throw std::runtime_error("Error");  // mutex automatically unlocked
}

Deadlock with nested locks

// BAD: deadlock with regular Mutex
Mutex mutex;

void outer()
{
    mutex.lock();
    inner();  // Deadlock! Same thread tries to lock again
    mutex.unlock();
}

void inner()
{
    mutex.lock();
    // ...
    mutex.unlock();
}

// GOOD: use RecursiveMutex when recursion is needed
RecursiveMutex mutex;  // Now safe

Circular lock dependencies

// BAD: potential deadlock
Mutex mutex1, mutex2;

// Thread A
mutex1.lock();
mutex2.lock();  // Waits if Thread B holds mutex2

// Thread B
mutex2.lock();
mutex1.lock();  // Waits if Thread A holds mutex1

// GOOD: consistent lock order
// Both threads acquire in same order: mutex1 then mutex2

Summary

FeatureSupported
Basic mutex✅
Recursive mutex✅
Shared memory mutex✅
RAII lock management✅
Non-blocking try-lock✅
Robust process-shared✅
Native handle access✅