In order to use the deadlock detection debug mode, ensure that _JUST_THREAD_DEADLOCK_CHECK is defined before
any of the just::thread
headers are included. It is important that this macro is defined in
all translation units that include just::thread
headers if it is defined in any, in order to avoid link-time errors.
When a deadlock occurs, the library will report the full stack trace of the threads involved in the deadlock, so you can see the exact location of the problem. Crucially, the library will also report the thread and callstack for where the ownership of the synchronization objects was acquired.
On Windows, if the library detects deadlock it will then display message boxes showing the full stack trace of the threads involved in the deadlock, before breaking into the debugger. For the most useful stack trace information, the executable should be built with debug information.
On Linux, the deadlock message and stack trace is written to the standard error
stream, and then std::terminate()
is called. For the most useful stack trace information the executable should
be built with both the -g switch and the -rdynamic
switch:
g++ -std=c++0x -pthread -D_JUST_THREAD_DEADLOCK_CHECK -g -rdynamic -o sample sample.cpp -ljustthread -lrt
Example
Take a look at the following code:
#include <thread> #include <mutex> #include <iostream> std::mutex io_mutex; void thread_func() { std::lock_guard<std::mutex> lk(io_mutex); std::cout<<"Hello from thread_func"<<std::endl; } int main() { std::thread t(thread_func); std::lock_guard<std::mutex> lk(io_mutex); std::cout<<"Hello from main thread"<<std::endl; t.join(); return 0; }
Now, it is obvious just from looking at the code that there's a potential deadlock
here: the main thread holds the lock on io_mutex
across the call to t.join(). Therefore,
if the main thread manages to lock the io_mutex
before the new thread does then the program will deadlock: the main thread
is waiting for thread_func
to complete, but thread_func
is blocked on the io_mutex,
which is held by the main thread!
If you compile this program and run it a few times without deadlock checking enabled then it will eventually hang when it hits the deadlock. On the other hand, if you compile it with deadlock checking enabled, then if deadlock does occur, then this will be detected by the library, and you will get output similar to the following:
Hello from main thread Deadlock Detected! Deadlock detected in thread 1 at: ./sample(__jss::__checked0x::__stack_trace::__stack_trace()+0x64) [0x40b6f4] Called from ./sample(__jss::__checked0x::mutex::lock()+0x35) [0x40d445] Called from ./sample(std::__jss_checked0x::lock_guard<std::__jss_checked0x::mutex>::lock_guard(std::__jss_checked0x::mutex&)+0x2a) [0x40837c] Called from ./sample(thread_func()+0x19) [0x407caf] Called from ./sample(__jss::__checked0x::__invoke_v<void, void (&)()>::operator()(void (*)())+0x16) [0x407efc] Called from ./sample(_ZN5__jss11__checked0x9__invokerIvRFvvEE11__do_invokeIEEvU10__variadicRT_+0x1c) [0x407f1a] Called from ./sample(_ZN5__jss11__checked0x10__unpackerINS0_12__args_tupleIEEE19__unpack_and_invokeINS0_9__invokerIvRFvvEEEEENT_13__result_typeEPSA_RKS3_U10__variadicRT0_+0x19) [0x407f35] Called from ./sample(__jss::__checked0x::__invoker<void, void (&)()>::operator()()+0x1d)[0x407f55] Called from ./sample(__jss::__checked0x::thread_data<__jss::__checked0x::__invoker<void,void (&)()> >::run()+0x19) [0x407f71] Called from ./sample(thread_start_function+0x1e4) [0x4090e4] Called from /lib/libpthread.so.0 [0x7f252f0f73ba] Called from /lib/libc.so.6(clone+0x6d) [0x7f252ee63fcd] Deadlock on object locked in thread 2 at: ./sample(__jss::__checked0x::__stack_trace::__stack_trace()+0x64) [0x40b6f4] Called from ./sample(__jss::__checked0x::mutex::lock()+0x35) [0x40d445] Called from ./sample(std::__jss_checked0x::lock_guard<std::__jss_checked0x::mutex>::lock_guard(std::__jss_checked0x::mutex&)+0x2a) [0x40837c] Called from ./sample(main+0x27) [0x407bfb] Called from /lib/libc.so.6(__libc_start_main+0xe6) [0x7f252ed9c5a6] Called from ./sample [0x407a89] Thread 2 blocked at: ./sample(__jss::__checked0x::__stack_trace::__stack_trace()+0x64) [0x40b6f4] Called from ./sample(std::__jss_checked0x::thread::join()+0x30) [0x408b60] Called from ./sample(main+0x4c) [0x407c20] Called from /lib/libc.so.6(__libc_start_main+0xe6) [0x7f252ed9c5a6] Called from ./sample [0x407a89] terminate called without an active exception Aborted
This output comes from a run under linux. The output on Windows is similar,
but will appear in a message box, and includes source line numbers. As you
can see, the stack trace tells us that thread 1 is blocked in a call to mutex::lock() from the lock_guard
constructor in thread_func(). Not only that, but it tells us that the
deadlock is on an object locked in thread 2, from a lock_guard
constructor in main, and that
thread 2 is blocked on a call to thread::join.
This is all the information we need to see the deadlock in this case, but in more complex cases we might need to go further up the call stack, particularly if the deadlock occurs in a function called from lots of different threads, or the mutex being used in the function depends on its parameters.
If you compile your program with deadlock detection enabled and run it under a debugger (or you have a Just-In-Time debugger enabled on Windows) then the program will break into the debugger when the deadlock is detected. This will enable you to inspect the state of the objects on the call stack.