1. Main thread
– Gets created during startup
– It’s possibly the only thread
– Normally sets up the app/program
– Often ends up in an event queue
2. Worker thread(s)
– Get spawned manually or borrowed from a pool
– Make it possible to solve tasks in parallel
– Release some load from the main thread
– Possibly work on an event/task queue
Example (with 1 main and 1 worker thread)
#include <iostream> #include <thread> #include <mutex> #include <condition_variable> #include <atomic> #include <queue> class Logger { public: static void log(const std::string & msg) { static std::mutex logMutex; std::lock_guard<std::mutex> lock(logMutex); std::cout << msg << std::endl; } }; class PiCalculator { public: PiCalculator() : m_stopRequested(false), m_mutex(), m_queue(), m_condVar(), m_calcCount(0), m_worker(std::thread([this](){run();})) {} ~PiCalculator() { if (m_worker.joinable()) { requestStop(); m_worker.join(); } } void addCalculation(unsigned long long & iter) { std::lock_guard<std::mutex> lock(m_mutex); m_queue.push(iter); m_condVar.notify_all(); } private: void requestStop() { std::lock_guard<std::mutex> lock(m_mutex); m_stopRequested = true; m_condVar.notify_all(); } void run() { unsigned long long iter = 0; unsigned int calcNum = 0; // Worker thread - Gets work from the task queue while (!m_stopRequested) { { // unique_lock necessary for condVar.wait() std::unique_lock<std::mutex> lock(m_mutex); while (!m_stopRequested && m_queue.empty()) { // Releases mutex while waiting passively m_condVar.wait(lock); } if (m_stopRequested) { return; } iter = m_queue.front(); calcNum = m_calcCount++; m_queue.pop(); } calculate(iter, calcNum); } } void calculate(unsigned long long iter, unsigned int calcNum) { // Worker thread - Executes a possibly long time task if (m_stopRequested) { return; } auto calcNumStr = std::to_string(calcNum); Logger::log("[" + calcNumStr + "]" + " Start calculation ..."); double sum = 0.0; int sign = 1; for (unsigned long long i = 0; i < iter && !m_stopRequested; ++i) { sum += sign / (2.0 * i + 1.0); sign *= -1; } if (m_stopRequested) { return; } auto piStr = std::to_string(4.0 * sum); Logger::log("[" + calcNumStr + "]" + " ... finished: " + piStr); } std::atomic<bool> m_stopRequested; std::mutex m_mutex; std::queue<unsigned long long> m_queue; std::condition_variable m_condVar; std::atomic<unsigned int> m_calcCount; std::thread m_worker; }; int main() { // Main thread - Sets up the program Logger::log("Calculating Pi with the Taylor method"); Logger::log(""); Logger::log("Possible Input:"); Logger::log("- <num> ... number of iterations"); Logger::log("- quit ... quits the process"); Logger::log(""); std::string input = ""; PiCalculator piCalculator; // Main thread - Enters the main event loop while (true) { std::cin >> input; if (input == "quit") { break; } try { unsigned long long iter = std::stoull(input); piCalculator.addCalculation(iter); } catch (...) { Logger::log("The input was invalid"); } } return 0; }
With C++20 you can use std::jthread (“Cooperatively Interruptible Joining Thread”), which does the interruption request and the joining in its destructor.
jthread::~jthread() { if (joinable()) // Not joined/detached yet { request_stop(); // Inform to stop asap join(); // Wait until finished } }