diff --git a/cpp/multithreading/CMakeLists.txt b/cpp/multithreading/CMakeLists.txt index ac834e4..a1e6b93 100644 --- a/cpp/multithreading/CMakeLists.txt +++ b/cpp/multithreading/CMakeLists.txt @@ -15,4 +15,5 @@ project( LANGUAGES CXX ) +add_subdirectory(deadlock) add_subdirectory(race-condition) diff --git a/cpp/multithreading/deadlock/CMakeLists.txt b/cpp/multithreading/deadlock/CMakeLists.txt new file mode 100644 index 0000000..a6f8373 --- /dev/null +++ b/cpp/multithreading/deadlock/CMakeLists.txt @@ -0,0 +1,26 @@ +################################################################################ +## Author: Shaun Reed ## +## Legal: All Content (c) 2022 Shaun Reed, all rights reserved ## +## About: An example and solution for deadlocks in C++ ## +## ## +## Contact: shaunrd0@gmail.com | URL: www.shaunreed.com | GitHub: shaunrd0 ## +################################################################################ + +cmake_minimum_required(VERSION 3.16) + +# std::scoped_lock requires C++17 +set(CMAKE_CXX_STANDARD 17) +add_compile_options("-Wall") + +project( + #[[NAME]] Deadlock + VERSION 1.0 + DESCRIPTION "Example and solution for deadlocks in C++" + LANGUAGES CXX +) + +add_executable( + multithread-deadlock driver.cpp +) + +target_link_libraries(multithread-deadlock pthread) diff --git a/cpp/multithreading/deadlock/driver.cpp b/cpp/multithreading/deadlock/driver.cpp new file mode 100644 index 0000000..ad3e101 --- /dev/null +++ b/cpp/multithreading/deadlock/driver.cpp @@ -0,0 +1,189 @@ +/*############################################################################## +## Author: Shaun Reed ## +## Legal: All Content (c) 2022 Shaun Reed, all rights reserved ## +## About: An example and solution for deadlocks in C++ ## +## ## +## Contact: shaunrd0@gmail.com | URL: www.shaunreed.com | GitHub: shaunrd0 ## +################################################################################ +*/ + +#include +#include +#include +#include +#include + +static std::mutex mtx_A, mtx_B, output; + +// Helper function to output thread ID and string associated with mutex name +// + This must also be thread-safe, since we want threads to produce output +// + There is no bug or issue here; This is just in support of example output +void print_safe(const std::string & s) { + std::scoped_lock scopedLock(output); + std::cout << s << std::endl; +} + +// Helper function to convert std::thread::id to string +std::string id_string(const std::thread::id & id) { + std::stringstream stream; + stream << id; + return stream.str(); +} + +// In the two threads within this function, we have a problem +// + The mutex locks are acquired in reverse order, so they collide +// + This is called a deadlock; The program will *never* finish +void problem() { + std::thread thread_A([]()->void { + mtx_A.lock(); + print_safe(id_string(std::this_thread::get_id()) + " thread_A: Locked A"); + std::this_thread::sleep_for(std::chrono::seconds(1)); + mtx_B.lock(); // We can't lock B! thread_B is using it + // The program will never reach this point in execution; We are in deadlock + print_safe(id_string(std::this_thread::get_id()) + + " thread_A: B has been unlocked, we can proceed!\n Locked B" + ); + std::this_thread::sleep_for(std::chrono::seconds(1)); + + print_safe(id_string(std::this_thread::get_id()) + + " thread_A: Unlocking A, B..." + ); + mtx_A.unlock(); + mtx_B.unlock(); + }); + + std::thread thread_B([]()->void { + mtx_B.lock(); + print_safe(id_string(std::this_thread::get_id()) + " thread_B: Locked B"); + std::this_thread::sleep_for(std::chrono::seconds(1)); + mtx_A.lock(); // We can't lock A! thread_A is using it + // The program will never reach this point in execution; We are in deadlock + print_safe(id_string(std::this_thread::get_id()) + + " thread_B: A has been unlocked, we can proceed!\n Locked A" + ); + std::this_thread::sleep_for(std::chrono::seconds(1)); + + print_safe(id_string(std::this_thread::get_id()) + + " thread_B: Unlocking B, A..." + ); + mtx_B.unlock(); + mtx_A.unlock(); + }); + + // This offers a way out of the deadlock, so we can proceed to the solution + std::this_thread::sleep_for(std::chrono::seconds(2)); + char input; + print_safe("\n" + + id_string(std::this_thread::get_id()) + + " problem(): We are in a deadlock. \n" + + " Enter y/Y to continue to the solution...\n" + ); + while (std::cin >> input) { + if (input != 'Y' && input != 'y') continue; + else break; + } + print_safe(id_string(std::this_thread::get_id()) + + " problem(): Unlocking A, B..." + ); + mtx_A.unlock(); + mtx_B.unlock(); + + thread_A.join(); + thread_B.join(); +} + +// std::lock will lock N mutex locks +// + If either is in use, execution will block until both are available to lock +void solution_A() { + std::thread thread_A([]()->void { + std::lock(mtx_A, mtx_B); + print_safe(id_string(std::this_thread::get_id()) + ": Locked A, B"); + std::this_thread::sleep_for(std::chrono::seconds(1)); + + print_safe(id_string(std::this_thread::get_id()) + ": Unlocking A, B..."); + mtx_A.unlock(); + mtx_B.unlock(); + }); + + std::thread thread_B([]()->void { + std::lock(mtx_B, mtx_A); + print_safe(id_string(std::this_thread::get_id()) + ": Locked B, A"); + std::this_thread::sleep_for(std::chrono::seconds(1)); + + print_safe(id_string(std::this_thread::get_id()) + ": Unlocking B, A..."); + mtx_B.unlock(); + mtx_A.unlock(); + }); + + thread_A.join(); + thread_B.join(); +} + +// std::lock_guard is a C++11 object which can be constructed with 1 mutex +// + When the program leaves the scope of the guard, the mutex is unlocked +void solution_B() { + std::thread thread_A([]()->void { + // lock_guard will handle unlocking when program leaves this scope + std::lock_guard guard_A(mtx_A), guard_B(mtx_B); + print_safe(id_string(std::this_thread::get_id()) + ": Locked A, B"); + std::this_thread::sleep_for(std::chrono::seconds(1)); + + print_safe(id_string(std::this_thread::get_id()) + ": Unlocking A, B..."); + // We don't need to explicitly unlock either mutex + }); + + std::thread thread_B([]()->void { + std::lock_guard guard_B(mtx_B), guard_A(mtx_A); + print_safe(id_string(std::this_thread::get_id()) + ": Locked B, A"); + std::this_thread::sleep_for(std::chrono::seconds(1)); + + print_safe(id_string(std::this_thread::get_id()) + ": Unlocking B, A..."); + // We don't need to explicitly unlock either mutex + }); + + thread_A.join(); + thread_B.join(); +} + +// std::scoped_lock is a C++17 object that can be constructed with N mutex +// + When the program leaves this scope, all N mutex will be unlocked +void solution_C() { + std::thread thread_A([]()->void { + // scoped_lock will handle unlocking when program leaves this scope + std::scoped_lock scopedLock(mtx_A, mtx_B); + print_safe(id_string(std::this_thread::get_id()) + ": Locked A, B"); + std::this_thread::sleep_for(std::chrono::seconds(1)); + + print_safe(id_string(std::this_thread::get_id()) + ": Unlocking A, B..."); + // We don't need to explicitly unlock either mutex + }); + + std::thread thread_B([]()->void { + std::scoped_lock scopedLock(mtx_A, mtx_B); + print_safe(id_string(std::this_thread::get_id()) + ": Locked B, A"); + std::this_thread::sleep_for(std::chrono::seconds(1)); + + print_safe(id_string(std::this_thread::get_id()) + ": Unlocking B, A..."); + // We don't need to explicitly unlock either mutex + }); + + thread_A.join(); + thread_B.join(); +} + +int main(const int argc, const char * argv[]) { + std::cout << "main() thread id: " << std::this_thread::get_id() << std::endl; + + problem(); + + print_safe("\nsolution_A, using std::lock\n"); + solution_A(); + + print_safe("\nsolution_B, using std::lock_guard\n"); + solution_B(); + + print_safe("\nsolution_C, using std::scoped_lock\n"); + solution_C(); + + return 0; +}