From aa36be61a2c8f7427be2cec55dfda843d03e5d4e Mon Sep 17 00:00:00 2001 From: Barak Shoshany Date: Sat, 24 Apr 2021 11:24:42 -0400 Subject: [PATCH] Updated to v1.1 --- LICENSE.txt | 42 +-- README.md | 46 +-- thread_pool.hpp | 732 ++++++++++++++++++++++++------------------------ 3 files changed, 411 insertions(+), 409 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index 118ef11..1eeadcc 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2021 Barak Shoshany - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2021 Barak Shoshany + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index c212f73..ac8ce6e 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,25 @@ - # A simple but powerful C++17 thread pool class - + -- [A simple but powerful C++17 thread pool class](#a-simple-but-powerful-c17-thread-pool-class) - - [Introduction](#introduction) - - [Features](#features) - - [Basic usage](#basic-usage) - - [Including the library](#including-the-library) - - [Constructors](#constructors) - - [Submitting tasks to the queue](#submitting-tasks-to-the-queue) - - [Parallelizing loops](#parallelizing-loops) - - [Advanced usage](#advanced-usage) - - [Getting and resetting the number of threads in the pool](#getting-and-resetting-the-number-of-threads-in-the-pool) - - [Submitting tasks to the queue without futures](#submitting-tasks-to-the-queue-without-futures) - - [Manually waiting for all tasks to complete](#manually-waiting-for-all-tasks-to-complete) - - [Synchronizing printing to an output stream](#synchronizing-printing-to-an-output-stream) - - [Motivation](#motivation) - - [The synced stream class](#the-synced-stream-class) - - [Compiling](#compiling) - - [Version history](#version-history) - - [Feedback](#feedback) - - [Author and copyright](#author-and-copyright) +- [Introduction](#introduction) +- [Features](#features) +- [Basic usage](#basic-usage) + - [Including the library](#including-the-library) + - [Constructors](#constructors) + - [Submitting tasks to the queue](#submitting-tasks-to-the-queue) + - [Parallelizing loops](#parallelizing-loops) +- [Advanced usage](#advanced-usage) + - [Getting and resetting the number of threads in the pool](#getting-and-resetting-the-number-of-threads-in-the-pool) + - [Submitting tasks to the queue without futures](#submitting-tasks-to-the-queue-without-futures) + - [Manually waiting for all tasks to complete](#manually-waiting-for-all-tasks-to-complete) +- [Synchronizing printing to an output stream](#synchronizing-printing-to-an-output-stream) + - [Motivation](#motivation) + - [The synced stream class](#the-synced-stream-class) +- [Compiling](#compiling) +- [Version history](#version-history) +- [Feedback](#feedback) +- [Author and copyright](#author-and-copyright) @@ -276,11 +274,13 @@ This library was tested on the following compilers and platforms: * Clang 11.0.0 on Windows 10 build 19042.746 and Ubuntu 20.04.1 LTS. * MSVC v14.28.29333 on Windows 10 build 19042.746. -As this library requires C++17 features, the code must be compiled with C++17 support. For GCC and Clang, use the `-std=c++17` flag. For MSVC, use `/std:c++17`. Additionally, on Linux, you may need to pass `-pthread` to GCC and Clang to enable the POSIX threads library. +As this library requires C++17 features, the code must be compiled with C++17 support. For GCC and Clang, use the `-std=c++17` flag. For MSVC, use `/std:c++17`. On Linux, you may need to pass `-pthread` to GCC and Clang to enable the POSIX threads library. ## Version history +* Version 1.1 (2021-04-24) + * Cosmetic changes only. Fixed a typo in the Doxygen comments and added a link to the GitHub repository. * Version 1.0 (2021-01-15) * Initial release. @@ -293,3 +293,5 @@ If you would like a request any additional features, or if you encounter any bug ## Author and copyright Copyright (c) 2021 [Barak Shoshany](http://baraksh.com) (baraksh@gmail.com). Licensed under the [MIT license](LICENSE.txt). + +If you use this class in your code, please acknowledge the author and provide a link to the [GitHub repository](https://github.com/bshoshany/thread-pool). Thank you! diff --git a/thread_pool.hpp b/thread_pool.hpp index 2e4dc20..81b42a3 100644 --- a/thread_pool.hpp +++ b/thread_pool.hpp @@ -1,366 +1,366 @@ -#pragma once - -/** - * @file matrix.hpp - * @author Barak Shoshany (baraksh@gmail.com) (http://baraksh.com) - * @version 1.0 - * @date 2021-01-15 - * @copyright Copyright (c) 2021 - * - * @brief A simple but powerful thread pool class. Please see the attached README.md file for more information and examples. - */ - -#include // std::max -#include // std::atomic -#include // std::uint_fast32_t -#include // std::function -#include // std::promise -#include // std::cout, std::ostream -#include // std::shared_ptr, std::unique_ptr -#include // std::mutex, std::scoped_lock -#include // std::queue -#include // std::this_thread, std::thread -#include // std::decay_t, std::enable_if_t, std::is_void_v, std::invoke_result_t -#include // std::move, std::swap - -/** - * @brief A simple but powerful thread pool class. Maintains a queue of tasks, which are executed by threads in the pool as they become available. - */ -class thread_pool -{ - typedef std::uint_fast32_t ui32; - -public: - // ============================ - // Constructors and destructors - // ============================ - - /** - * @brief Construct a new thread pool. - * - * @param _thread_count The number of threads to use. Default value is the total number of hardware threads available, as reported by the implementation. With a hyperthreaded CPU, this will be twice the number of CPU cores. If the argument is zero, 1 thread will be used. - */ - thread_pool(const ui32 &_thread_count = std::thread::hardware_concurrency()) - : thread_count(std::max(_thread_count, 1)), threads(new std::thread[std::max(_thread_count, 1)]) - { - create_threads(); - } - - /** - * @brief Destruct the thread pool. Waits for all submitted tasks to be completed, then destroys all threads. - */ - ~thread_pool() - { - wait_for_tasks(); - running = false; - destroy_threads(); - } - - // ======================= - // Public member functions - // ======================= - - /** - * @brief Get the number of threads in the pool. - * - * @return The number of threads. - */ - ui32 get_thread_count() const - { - return thread_count; - } - - /** - * @brief Parallelize a loop by splitting it into blocks, submitting each block separately to the thread pool, and waiting for all blocks to finish executing. The loop will be equivalent to "for (T i = first_index; i <= last_index; i++) loop(i);". - * - * @tparam T The type of the loop index. Should be a signed or unsigned integer. - * @tparam F The type of the function to loop through. - * @param first_index The first index in the loop (inclusive). - * @param last_index The last index in the loop (inclusive). - * @param loop The function to loop through. Should take exactly one argument, the loop index. - * @param num_tasks The maximum number of tasks to split the loop into. Default is to use the number of threads in the pool. - */ - template - void parallelize_loop(T first_index, T last_index, const F &loop, ui32 num_tasks = 0) - { - if (num_tasks == 0) - num_tasks = thread_count; - if (last_index < first_index) - std::swap(last_index, first_index); - size_t total_size = last_index - first_index + 1; - size_t block_size = total_size / num_tasks; - if (block_size == 0) - { - block_size = 1; - num_tasks = std::max((ui32)1, (ui32)total_size); - } - std::atomic blocks_running = 0; - for (ui32 t = 0; t < num_tasks; t++) - { - T start = (T)(t * block_size + first_index); - T end = (t == num_tasks - 1) ? last_index : (T)((t + 1) * block_size + first_index - 1); - std::cout << start << '-' << end << '\n'; - blocks_running++; - push_task([&start, &end, &loop, &blocks_running] { - for (T i = start; i <= end; i++) - loop(i); - blocks_running--; - }); - while (blocks_running != 0) - { - std::this_thread::yield(); - } - } - } - - /** - * @brief Push a function with no arguments or return value into the task queue. - * - * @tparam F The type of the function. - * @param task The function to push. - */ - template - void push_task(const F &task) - { - tasks_waiting++; - { - const std::scoped_lock lock(queue_mutex); - tasks.push(std::move(std::function(task))); - } - } - - /** - * @brief Push a function with arguments, but no return value, into the task queue. - * @details The function is wrapped inside a lambda in order to hide the arguments, as the tasks in the queue must be of type std::function, so they cannot have any arguments or return value. If no arguments are provided, the other overload will be used, in order to avoid the (slight) overhead of using a lambda. - * - * @tparam F The type of the function. - * @tparam A The types of the arguments. - * @param task The function to push. - * @param args The arguments to pass to the function. - */ - template - void push_task(const F &task, const A &... args) - { - push_task([task, args...] { task(args...); }); - } - - /** - * @brief Reset the number of threads in the pool. Waits for all submitted tasks to be completed, then destroys all threads and creates a new thread pool with the new number of threads. - * - * @param _thread_count The number of threads to use. Default value is the total number of hardware threads available, as reported by the implementation. With a hyperthreaded CPU, this will be twice the number of CPU cores. If the argument is zero, 1 thread will be used. - */ - void reset(const ui32 &_thread_count = std::thread::hardware_concurrency()) - { - wait_for_tasks(); - running = false; - destroy_threads(); - thread_count = std::max(_thread_count, 1); - threads.reset(new std::thread[std::max(_thread_count, 1)]); - running = true; - create_threads(); - } - - /** - * @brief Submit a function with zero or more arguments and no return value into the task queue, and get an std::future that will be set to true upon completion of the task. - * - * @tparam F The type of the function. - * @tparam A The types of the zero or more arguments to pass to the function. - * @param task The function to submit. - * @param args The zero or more arguments to pass to the function. - * @return A future to be used later to check if the function has finished its execution. - */ - template , std::decay_t...>>>> - std::future submit(const F &task, const A &... args) - { - std::shared_ptr> promise(new std::promise); - std::future future = promise->get_future(); - push_task([task, args..., promise] { - task(args...); - promise->set_value(true); - }); - return future; - } - - /** - * @brief Submit a function with zero or more arguments and a return value into the task queue, and get a future for its eventual returned value. - * - * @tparam F The type of the function. - * @tparam A The types of the zero or more arguments to pass to the function. - * @tparam R The return type of the function. - * @param task The function to submit. - * @param args The zero or more arguments to pass to the function. - * @return A future to be used later to obtain the function's returned value, waiting for it to finish its execution if needed. - */ - template , std::decay_t...>, typename = std::enable_if_t>> - std::future submit(const F &task, const A &... args) - { - std::shared_ptr> promise(new std::promise); - std::future future = promise->get_future(); - push_task([task, args..., promise] { - promise->set_value(task(args...)); - }); - return future; - } - - /** - * @brief Wait for all submitted tasks to be completed - both those that are currently being executed by threads, and those that are still waiting in the queue. To wait for a specific task, use push_task_future instead. - */ - void wait_for_tasks() - { - while (tasks_waiting != 0) - { - std::this_thread::yield(); - } - } - -private: - // ======================== - // Private member functions - // ======================== - - /** - * @brief Create the threads in the pool and assign a worker to each thread. - */ - void create_threads() - { - for (ui32 i = 0; i < thread_count; i++) - { - threads[i] = std::thread(&thread_pool::worker, this); - } - } - - /** - * @brief Destroy the threads in the pool by joining them. - */ - void destroy_threads() - { - for (ui32 i = 0; i < thread_count; i++) - { - threads[i].join(); - } - } - - /** - * @brief Try to pop a new task out of the queue. - * - * @param task A reference to the task. Will be populated with a function if the queue is not empty. - * @return true if a task was found, false if the queue is empty. - */ - bool pop_task(std::function &task) - { - const std::scoped_lock lock(queue_mutex); - if (tasks.empty()) - return false; - else - { - task = std::move(tasks.front()); - tasks.pop(); - return true; - } - } - - /** - * @brief A worker function to be assigned to each thread in the pool. Pops tasks out of the queue and executes them, until the atomic variable running is set to false. - */ - void worker() - { - while (running) - { - std::function task; - if (pop_task(task)) - { - task(); - tasks_waiting--; - } - else - { - std::this_thread::yield(); - } - } - } - - // ============ - // Private data - // ============ - - /** - * @brief An atomic variable indicating to the workers to keep running. - */ - std::atomic running = true; - - /** - * @brief An atomic variable to keep track of how many tasks are currently waiting to finish - either still in the queue, or running in a thread. - */ - std::atomic tasks_waiting = 0; - - /** - * @brief A mutex to synchronize access to the task queue by different threads. - */ - mutable std::mutex queue_mutex; - - /** - * @brief A queue of tasks to be executed by the threads. - */ - std::queue> tasks; - - /** - * @brief The number of threads in the pool. - */ - ui32 thread_count; - - /** - * @brief A smart pointer to manage the memory allocated for the threads. - */ - std::unique_ptr threads; -}; - -/** - * @brief A class to synchronize printing to an output stream by different threads. - */ -class synced_stream -{ -public: - /** - * @brief Construct a new synced stream. - * - * @param _out_stream The output stream to sync to. Default is std::cout. - */ - synced_stream(std::ostream &_out_stream = std::cout) - : out_stream(_out_stream){}; - - /** - * @brief Print any number of items into the output stream. Ensures that no other threads print to this stream simultaneously, as long as they all use this synced_stream object to print. - * - * @tparam T The types of the items - * @param items The items to print. - */ - template - void print(const T &... items) - { - const std::scoped_lock lock(stream_mutex); - (out_stream << ... << items); - } - - /** - * @brief Print any number of items into the output stream, followed by a newline character. Ensures that no other threads print to this stream simultaneously, as long as they all use this synced_stream object to print. - * - * @tparam T The types of the items - * @param items The items to print. - */ - template - void println(const T &... items) - { - print(items..., '\n'); - } - -private: - /** - * @brief A mutex to synchronize printing. - */ - mutable std::mutex stream_mutex; - - /** - * @brief The output stream to print to. - */ - std::ostream &out_stream; -}; +#pragma once + +/** + * @file thread_pool.hpp + * @author Barak Shoshany (baraksh@gmail.com) (http://baraksh.com) + * @version 1.1 + * @date 2021-04-24 + * @copyright Copyright (c) 2021 Barak Shoshany. Licensed under the MIT license. + * + * @brief A simple but powerful C++17 thread pool class. Please visit the GitHub repository at https://github.com/bshoshany/thread-pool for documentation and updates, or to submit feature requests and bug reports. + */ + +#include // std::max +#include // std::atomic +#include // std::uint_fast32_t +#include // std::function +#include // std::promise +#include // std::cout, std::ostream +#include // std::shared_ptr, std::unique_ptr +#include // std::mutex, std::scoped_lock +#include // std::queue +#include // std::this_thread, std::thread +#include // std::decay_t, std::enable_if_t, std::is_void_v, std::invoke_result_t +#include // std::move, std::swap + +/** + * @brief A simple but powerful thread pool class. Maintains a queue of tasks, which are executed by threads in the pool as they become available. + */ +class thread_pool +{ + typedef std::uint_fast32_t ui32; + +public: + // ============================ + // Constructors and destructors + // ============================ + + /** + * @brief Construct a new thread pool. + * + * @param _thread_count The number of threads to use. Default value is the total number of hardware threads available, as reported by the implementation. With a hyperthreaded CPU, this will be twice the number of CPU cores. If the argument is zero, 1 thread will be used. + */ + thread_pool(const ui32 &_thread_count = std::thread::hardware_concurrency()) + : thread_count(std::max(_thread_count, 1)), threads(new std::thread[std::max(_thread_count, 1)]) + { + create_threads(); + } + + /** + * @brief Destruct the thread pool. Waits for all submitted tasks to be completed, then destroys all threads. + */ + ~thread_pool() + { + wait_for_tasks(); + running = false; + destroy_threads(); + } + + // ======================= + // Public member functions + // ======================= + + /** + * @brief Get the number of threads in the pool. + * + * @return The number of threads. + */ + ui32 get_thread_count() const + { + return thread_count; + } + + /** + * @brief Parallelize a loop by splitting it into blocks, submitting each block separately to the thread pool, and waiting for all blocks to finish executing. The loop will be equivalent to "for (T i = first_index; i <= last_index; i++) loop(i);". + * + * @tparam T The type of the loop index. Should be a signed or unsigned integer. + * @tparam F The type of the function to loop through. + * @param first_index The first index in the loop (inclusive). + * @param last_index The last index in the loop (inclusive). + * @param loop The function to loop through. Should take exactly one argument, the loop index. + * @param num_tasks The maximum number of tasks to split the loop into. Default is to use the number of threads in the pool. + */ + template + void parallelize_loop(T first_index, T last_index, const F &loop, ui32 num_tasks = 0) + { + if (num_tasks == 0) + num_tasks = thread_count; + if (last_index < first_index) + std::swap(last_index, first_index); + size_t total_size = last_index - first_index + 1; + size_t block_size = total_size / num_tasks; + if (block_size == 0) + { + block_size = 1; + num_tasks = std::max((ui32)1, (ui32)total_size); + } + std::atomic blocks_running = 0; + for (ui32 t = 0; t < num_tasks; t++) + { + T start = (T)(t * block_size + first_index); + T end = (t == num_tasks - 1) ? last_index : (T)((t + 1) * block_size + first_index - 1); + std::cout << start << '-' << end << '\n'; + blocks_running++; + push_task([&start, &end, &loop, &blocks_running] { + for (T i = start; i <= end; i++) + loop(i); + blocks_running--; + }); + while (blocks_running != 0) + { + std::this_thread::yield(); + } + } + } + + /** + * @brief Push a function with no arguments or return value into the task queue. + * + * @tparam F The type of the function. + * @param task The function to push. + */ + template + void push_task(const F &task) + { + tasks_waiting++; + { + const std::scoped_lock lock(queue_mutex); + tasks.push(std::move(std::function(task))); + } + } + + /** + * @brief Push a function with arguments, but no return value, into the task queue. + * @details The function is wrapped inside a lambda in order to hide the arguments, as the tasks in the queue must be of type std::function, so they cannot have any arguments or return value. If no arguments are provided, the other overload will be used, in order to avoid the (slight) overhead of using a lambda. + * + * @tparam F The type of the function. + * @tparam A The types of the arguments. + * @param task The function to push. + * @param args The arguments to pass to the function. + */ + template + void push_task(const F &task, const A &...args) + { + push_task([task, args...] { task(args...); }); + } + + /** + * @brief Reset the number of threads in the pool. Waits for all submitted tasks to be completed, then destroys all threads and creates a new thread pool with the new number of threads. + * + * @param _thread_count The number of threads to use. Default value is the total number of hardware threads available, as reported by the implementation. With a hyperthreaded CPU, this will be twice the number of CPU cores. If the argument is zero, 1 thread will be used. + */ + void reset(const ui32 &_thread_count = std::thread::hardware_concurrency()) + { + wait_for_tasks(); + running = false; + destroy_threads(); + thread_count = std::max(_thread_count, 1); + threads.reset(new std::thread[std::max(_thread_count, 1)]); + running = true; + create_threads(); + } + + /** + * @brief Submit a function with zero or more arguments and no return value into the task queue, and get an std::future that will be set to true upon completion of the task. + * + * @tparam F The type of the function. + * @tparam A The types of the zero or more arguments to pass to the function. + * @param task The function to submit. + * @param args The zero or more arguments to pass to the function. + * @return A future to be used later to check if the function has finished its execution. + */ + template , std::decay_t...>>>> + std::future submit(const F &task, const A &...args) + { + std::shared_ptr> promise(new std::promise); + std::future future = promise->get_future(); + push_task([task, args..., promise] { + task(args...); + promise->set_value(true); + }); + return future; + } + + /** + * @brief Submit a function with zero or more arguments and a return value into the task queue, and get a future for its eventual returned value. + * + * @tparam F The type of the function. + * @tparam A The types of the zero or more arguments to pass to the function. + * @tparam R The return type of the function. + * @param task The function to submit. + * @param args The zero or more arguments to pass to the function. + * @return A future to be used later to obtain the function's returned value, waiting for it to finish its execution if needed. + */ + template , std::decay_t...>, typename = std::enable_if_t>> + std::future submit(const F &task, const A &...args) + { + std::shared_ptr> promise(new std::promise); + std::future future = promise->get_future(); + push_task([task, args..., promise] { + promise->set_value(task(args...)); + }); + return future; + } + + /** + * @brief Wait for all submitted tasks to be completed - both those that are currently being executed by threads, and those that are still waiting in the queue. To wait for a specific task, use push_task_future instead. + */ + void wait_for_tasks() + { + while (tasks_waiting != 0) + { + std::this_thread::yield(); + } + } + +private: + // ======================== + // Private member functions + // ======================== + + /** + * @brief Create the threads in the pool and assign a worker to each thread. + */ + void create_threads() + { + for (ui32 i = 0; i < thread_count; i++) + { + threads[i] = std::thread(&thread_pool::worker, this); + } + } + + /** + * @brief Destroy the threads in the pool by joining them. + */ + void destroy_threads() + { + for (ui32 i = 0; i < thread_count; i++) + { + threads[i].join(); + } + } + + /** + * @brief Try to pop a new task out of the queue. + * + * @param task A reference to the task. Will be populated with a function if the queue is not empty. + * @return true if a task was found, false if the queue is empty. + */ + bool pop_task(std::function &task) + { + const std::scoped_lock lock(queue_mutex); + if (tasks.empty()) + return false; + else + { + task = std::move(tasks.front()); + tasks.pop(); + return true; + } + } + + /** + * @brief A worker function to be assigned to each thread in the pool. Pops tasks out of the queue and executes them, until the atomic variable running is set to false. + */ + void worker() + { + while (running) + { + std::function task; + if (pop_task(task)) + { + task(); + tasks_waiting--; + } + else + { + std::this_thread::yield(); + } + } + } + + // ============ + // Private data + // ============ + + /** + * @brief An atomic variable indicating to the workers to keep running. + */ + std::atomic running = true; + + /** + * @brief An atomic variable to keep track of how many tasks are currently waiting to finish - either still in the queue, or running in a thread. + */ + std::atomic tasks_waiting = 0; + + /** + * @brief A mutex to synchronize access to the task queue by different threads. + */ + mutable std::mutex queue_mutex; + + /** + * @brief A queue of tasks to be executed by the threads. + */ + std::queue> tasks; + + /** + * @brief The number of threads in the pool. + */ + ui32 thread_count; + + /** + * @brief A smart pointer to manage the memory allocated for the threads. + */ + std::unique_ptr threads; +}; + +/** + * @brief A class to synchronize printing to an output stream by different threads. + */ +class synced_stream +{ +public: + /** + * @brief Construct a new synced stream. + * + * @param _out_stream The output stream to sync to. Default is std::cout. + */ + synced_stream(std::ostream &_out_stream = std::cout) + : out_stream(_out_stream){}; + + /** + * @brief Print any number of items into the output stream. Ensures that no other threads print to this stream simultaneously, as long as they all use this synced_stream object to print. + * + * @tparam T The types of the items + * @param items The items to print. + */ + template + void print(const T &...items) + { + const std::scoped_lock lock(stream_mutex); + (out_stream << ... << items); + } + + /** + * @brief Print any number of items into the output stream, followed by a newline character. Ensures that no other threads print to this stream simultaneously, as long as they all use this synced_stream object to print. + * + * @tparam T The types of the items + * @param items The items to print. + */ + template + void println(const T &...items) + { + print(items..., '\n'); + } + +private: + /** + * @brief A mutex to synchronize printing. + */ + mutable std::mutex stream_mutex; + + /** + * @brief The output stream to print to. + */ + std::ostream &out_stream; +};