Date: Wed, 10 Jun 2026 10:33:00 +0100
On Sun, Jun 7, 2026 at 6:27 PM Giuseppe D'Angelo wrote:
>
> Deadlocking across *different* threads is well-defined behavior
<snip>
> An implementation that detects this case, and makes lock() throw the
> corresponding exception, is completely conforming.
<snip>
> In practice, I don't know of any std::mutex implementation that actually
> does this.
I think it could be coded something like this:
https://godbolt.org/z/6a6Es514q
And here it is copy-pasted:
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <cstdint>
#include <array>
#include <atomic>
#include <bitset>
#include <mutex>
#include <system_error>
#include <thread>
#include <tuple>
// --- System Constraints ---
constexpr std::size_t MAX_MUTEX_COUNT = 255u; // Maximum number of
mutexes in the system
constexpr std::size_t MAX_THREAD_COUNT = 127u; // Maximum number of
threads in the system
// --- Global State ---
// Tuple structure:
// 1. Address of the underlying mutex
// 2. Index of the thread which currently has the lock (0 means unowned)
// 3. Bitset for indices of threads waiting on the lock (sized by
MAX_THREAD_COUNT)
std::array<
std::tuple< std::mutex*, std::uintptr_t,
std::bitset<MAX_THREAD_COUNT + 1u> >,
MAX_MUTEX_COUNT + 1u
> locked;
// A meta-mutex to protect read/write access to the global 'locked' array
std::mutex meta_mtx;
// Helper to get a small, unique integer ID for each thread
std::uintptr_t get_small_thread_id(void) noexcept
{
static std::atomic<std::uintptr_t> thread_counter{0};
static thread_local std::uintptr_t local_id = ++thread_counter;
if ( local_id > MAX_THREAD_COUNT )
{
std::fputs("Too many threads!\n", stderr);
std::abort();
}
return local_id;
}
class Mutex {
typedef std::size_t size_t;
private:
std::mutex mtx;
size_t mutex_idx;
// Helper to get a unique integer ID for each mutex
static size_t get_next_mutex_idx(void) noexcept
{
static std::atomic<size_t> mutex_counter{0};
size_t id = ++mutex_counter;
if ( id > MAX_MUTEX_COUNT )
{
std::fputs("Too many mutices!\n", stderr);
std::abort();
}
return id;
}
public:
Mutex(void) : mutex_idx(get_next_mutex_idx())
{
std::lock_guard mylock(meta_mtx);
std::get<0>(locked[mutex_idx]) = &mtx; // Store address
std::get<1>(locked[mutex_idx]) = 0; // 0 = Unowned initially
std::get<2>(locked[mutex_idx]).reset(); // No threads waiting
}
void lock(void) noexcept(false) // might throw std::system_error
{
std::uintptr_t my_tid = get_small_thread_id();
{
// Lock the meta-state to check the deadlock chain safely
std::lock_guard<std::mutex> lock(meta_mtx);
size_t current_check_mutex = mutex_idx;
// Follow the chain of waiting threads
for (; /* ever */ ;)
{
std::uintptr_t owner = std::get<1>(locked[current_check_mutex]);
if ( 0u == owner )
{
break; // Nobody owns this mutex, the chain ends safely.
}
if ( owner == my_tid )
{
// Throw std::system_error specifically designed
for deadlocks
throw std::system_error(
std::make_error_code(std::errc::resource_deadlock_would_occur),
"Deadlock Detected! Cycle found in waiting chain."
);
}
// Is the owner thread waiting on any other mutex?
bool owner_is_waiting = false;
for ( size_t i = 0; i <= MAX_MUTEX_COUNT; ++i )
{
// Check the bitset to see if 'owner' is waiting
on mutex 'i'
if ( std::get<2>(locked[i]).test(owner) )
{
current_check_mutex = i; // Follow the chain
to the next mutex
owner_is_waiting = true;
break;
}
}
// If the owner isn't waiting on anything, the chain
ends safely.
if ( false == owner_is_waiting )
{
break;
}
}
// No deadlock detected. Register THIS thread as waiting
in the bitset.
std::get<2>(locked[mutex_idx]).set(my_tid);
} // meta_mtx is released here!
// Actually wait for the underlying lock
mtx.lock();
{
// We successfully acquired the lock. Update the meta-state.
std::lock_guard<std::mutex> lock(meta_mtx);
// We are no longer waiting
std::get<2>(locked[mutex_idx]).reset(my_tid);
// We are now the official owner
std::get<1>(locked[mutex_idx]) = my_tid;
}
}
void unlock(void) noexcept
{
{
std::lock_guard<std::mutex> lock(meta_mtx);
// Relinquish ownership (set back to 0)
std::get<1>(locked[mutex_idx]) = 0;
}
// Release the underlying mutex
mtx.unlock();
}
};
//=======================================================
// Here comes test code:
#include <chrono>
void ThreadEntryPoint(unsigned const index)
{
static Mutex m1, m2, m3;
Mutex* first = nullptr;
Mutex* second = nullptr;
switch ( index )
{
case 1u: first = &m1; second = &m2; break; // T1: m1 → m2
case 2u: first = &m2; second = &m3; break; // T2: m2 → m3
case 3u: first = &m3; second = &m1; break; // T3: m3 → m1
}
first->lock();
std::this_thread::sleep_for( std::chrono::milliseconds(200u) );
try
{
second->lock();
second->unlock();
}
catch ( std::system_error const& e )
{
std::fprintf(stderr, "Thread %u: %s\n", index, e.what());
}
first->unlock();
}
int main(void)
{
std::jthread a( ThreadEntryPoint, 1u );
std::jthread b( ThreadEntryPoint, 2u );
std::jthread c( ThreadEntryPoint, 3u );
}
>
> Deadlocking across *different* threads is well-defined behavior
<snip>
> An implementation that detects this case, and makes lock() throw the
> corresponding exception, is completely conforming.
<snip>
> In practice, I don't know of any std::mutex implementation that actually
> does this.
I think it could be coded something like this:
https://godbolt.org/z/6a6Es514q
And here it is copy-pasted:
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <cstdint>
#include <array>
#include <atomic>
#include <bitset>
#include <mutex>
#include <system_error>
#include <thread>
#include <tuple>
// --- System Constraints ---
constexpr std::size_t MAX_MUTEX_COUNT = 255u; // Maximum number of
mutexes in the system
constexpr std::size_t MAX_THREAD_COUNT = 127u; // Maximum number of
threads in the system
// --- Global State ---
// Tuple structure:
// 1. Address of the underlying mutex
// 2. Index of the thread which currently has the lock (0 means unowned)
// 3. Bitset for indices of threads waiting on the lock (sized by
MAX_THREAD_COUNT)
std::array<
std::tuple< std::mutex*, std::uintptr_t,
std::bitset<MAX_THREAD_COUNT + 1u> >,
MAX_MUTEX_COUNT + 1u
> locked;
// A meta-mutex to protect read/write access to the global 'locked' array
std::mutex meta_mtx;
// Helper to get a small, unique integer ID for each thread
std::uintptr_t get_small_thread_id(void) noexcept
{
static std::atomic<std::uintptr_t> thread_counter{0};
static thread_local std::uintptr_t local_id = ++thread_counter;
if ( local_id > MAX_THREAD_COUNT )
{
std::fputs("Too many threads!\n", stderr);
std::abort();
}
return local_id;
}
class Mutex {
typedef std::size_t size_t;
private:
std::mutex mtx;
size_t mutex_idx;
// Helper to get a unique integer ID for each mutex
static size_t get_next_mutex_idx(void) noexcept
{
static std::atomic<size_t> mutex_counter{0};
size_t id = ++mutex_counter;
if ( id > MAX_MUTEX_COUNT )
{
std::fputs("Too many mutices!\n", stderr);
std::abort();
}
return id;
}
public:
Mutex(void) : mutex_idx(get_next_mutex_idx())
{
std::lock_guard mylock(meta_mtx);
std::get<0>(locked[mutex_idx]) = &mtx; // Store address
std::get<1>(locked[mutex_idx]) = 0; // 0 = Unowned initially
std::get<2>(locked[mutex_idx]).reset(); // No threads waiting
}
void lock(void) noexcept(false) // might throw std::system_error
{
std::uintptr_t my_tid = get_small_thread_id();
{
// Lock the meta-state to check the deadlock chain safely
std::lock_guard<std::mutex> lock(meta_mtx);
size_t current_check_mutex = mutex_idx;
// Follow the chain of waiting threads
for (; /* ever */ ;)
{
std::uintptr_t owner = std::get<1>(locked[current_check_mutex]);
if ( 0u == owner )
{
break; // Nobody owns this mutex, the chain ends safely.
}
if ( owner == my_tid )
{
// Throw std::system_error specifically designed
for deadlocks
throw std::system_error(
std::make_error_code(std::errc::resource_deadlock_would_occur),
"Deadlock Detected! Cycle found in waiting chain."
);
}
// Is the owner thread waiting on any other mutex?
bool owner_is_waiting = false;
for ( size_t i = 0; i <= MAX_MUTEX_COUNT; ++i )
{
// Check the bitset to see if 'owner' is waiting
on mutex 'i'
if ( std::get<2>(locked[i]).test(owner) )
{
current_check_mutex = i; // Follow the chain
to the next mutex
owner_is_waiting = true;
break;
}
}
// If the owner isn't waiting on anything, the chain
ends safely.
if ( false == owner_is_waiting )
{
break;
}
}
// No deadlock detected. Register THIS thread as waiting
in the bitset.
std::get<2>(locked[mutex_idx]).set(my_tid);
} // meta_mtx is released here!
// Actually wait for the underlying lock
mtx.lock();
{
// We successfully acquired the lock. Update the meta-state.
std::lock_guard<std::mutex> lock(meta_mtx);
// We are no longer waiting
std::get<2>(locked[mutex_idx]).reset(my_tid);
// We are now the official owner
std::get<1>(locked[mutex_idx]) = my_tid;
}
}
void unlock(void) noexcept
{
{
std::lock_guard<std::mutex> lock(meta_mtx);
// Relinquish ownership (set back to 0)
std::get<1>(locked[mutex_idx]) = 0;
}
// Release the underlying mutex
mtx.unlock();
}
};
//=======================================================
// Here comes test code:
#include <chrono>
void ThreadEntryPoint(unsigned const index)
{
static Mutex m1, m2, m3;
Mutex* first = nullptr;
Mutex* second = nullptr;
switch ( index )
{
case 1u: first = &m1; second = &m2; break; // T1: m1 → m2
case 2u: first = &m2; second = &m3; break; // T2: m2 → m3
case 3u: first = &m3; second = &m1; break; // T3: m3 → m1
}
first->lock();
std::this_thread::sleep_for( std::chrono::milliseconds(200u) );
try
{
second->lock();
second->unlock();
}
catch ( std::system_error const& e )
{
std::fprintf(stderr, "Thread %u: %s\n", index, e.what());
}
first->unlock();
}
int main(void)
{
std::jthread a( ThreadEntryPoint, 1u );
std::jthread b( ThreadEntryPoint, 2u );
std::jthread c( ThreadEntryPoint, 3u );
}
Received on 2026-06-10 09:33:15
