diff --git a/cpp/multithreading/CMakeLists.txt b/cpp/multithreading/CMakeLists.txt index a1e6b93..fc9e2b8 100644 --- a/cpp/multithreading/CMakeLists.txt +++ b/cpp/multithreading/CMakeLists.txt @@ -16,4 +16,5 @@ project( ) add_subdirectory(deadlock) +add_subdirectory(livelock) add_subdirectory(race-condition) diff --git a/cpp/multithreading/livelock/CMakeLists.txt b/cpp/multithreading/livelock/CMakeLists.txt new file mode 100644 index 0000000..4b00f33 --- /dev/null +++ b/cpp/multithreading/livelock/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 livelocks 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]] LiveLock + VERSION 1.0 + DESCRIPTION "Example and solution for livelocks in C++" + LANGUAGES CXX +) + +add_executable( + multithread-livelock driver.cpp +) + +target_link_libraries(multithread-livelock pthread) diff --git a/cpp/multithreading/livelock/driver.cpp b/cpp/multithreading/livelock/driver.cpp new file mode 100644 index 0000000..5fb2cea --- /dev/null +++ b/cpp/multithreading/livelock/driver.cpp @@ -0,0 +1,117 @@ +/*############################################################################## +## Author: Shaun Reed ## +## Legal: All Content (c) 2022 Shaun Reed, all rights reserved ## +## About: An example and solution for livelocks 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; +} + +void problem() { + // Construct a vector with 5 agreed-upon times to synchronize loops in threads + typedef std::chrono::time_point time_point; + std::vector waitTime(6); + for (uint8_t i = 0; i < 6; i++) { + waitTime[i] = std::chrono::steady_clock::now()+std::chrono::seconds(1+i); + } + + std::thread thread_A([waitTime]()->void { + uint8_t count = 0; // Used to select time slot from waitTime vector + bool done = false; + while (!done) { + count++; + std::lock_guard l(mtx_A); + std::cout << std::this_thread::get_id() << " thread_A: Lock A\n"; + // Wait until the next time slot to continue + // + Helps to show example of livelock by ensuring B is not available + std::this_thread::sleep_until(waitTime[count]); + std::cout << std::this_thread::get_id() << " thread_A: Requesting B\n"; + if (mtx_B.try_lock()) { + done = true; + std::cout << std::this_thread::get_id() + << " thread_A: Acquired locks for A, B! Done.\n"; + } + else { + std::cout << std::this_thread::get_id() + << " thread_A: Can't lock B, unlocking A\n"; + } + } + mtx_B.unlock(); + }); + + std::thread thread_B([waitTime]()->void { + // As an example, enter livelock for only 5 iterations + // + Also used to select time slot from waitTime vector + uint8_t count = 0; + bool done = false; + while (!done && count < 5) { + count++; + std::lock_guard l(mtx_B); + // Wait until the next time slot to continue + // + Helps to show example of livelock by ensuring A is not available + std::this_thread::sleep_until(waitTime[count]); + if (mtx_A.try_lock()) { + // The program will never reach this point in the code + // + The only reason livelock ends is because count > 5 + done = true; + } + } + }); + + thread_A.join(); + thread_B.join(); +} + +// The solution below uses std::scoped_lock to avoid the livelock problem +void solution() { + std::thread thread_A([]()->void { + for (int i = 0; i < 5; i++) { + // Increase wait time with i + // + To encourage alternating lock ownership between threads + std::this_thread::sleep_for(std::chrono::milliseconds(100 * i)); + std::scoped_lock l(mtx_A, mtx_B); + std::cout << std::this_thread::get_id() + << " thread_A: Acquired locks for A, B!" << std::endl; + } + }); + + std::thread thread_B([]()->void { + for (int i = 0; i < 5; i++) { + std::this_thread::sleep_for(std::chrono::milliseconds(100 * i)); + std::scoped_lock l(mtx_B, mtx_A); + std::cout << std::this_thread::get_id() + << " thread_B: Acquired locks for B, A!" << std::endl; + } + }); + + 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(); + + std::cout << "\nSolution:\n\n"; + solution(); + + return 0; +}