mirror of
https://github.com/bshoshany/thread-pool.git
synced 2026-07-21 19:13:00 +04:00
first commit
This commit is contained in:
+21
@@ -0,0 +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.
|
||||
@@ -0,0 +1,295 @@
|
||||
<a id="markdown-a-simple-but-powerful-c17-thread-pool-class" name="a-simple-but-powerful-c17-thread-pool-class"></a>
|
||||
# A simple but powerful C++17 thread pool class
|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
- [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)
|
||||
|
||||
<!-- /TOC -->
|
||||
|
||||
<a id="markdown-introduction" name="introduction"></a>
|
||||
## Introduction
|
||||
|
||||
Multithreading is essential for modern high-performance computing. Since C++11, the C++ standard library has included built-in low-level multithreading support using constructs such as `std::thread`. However, `std::thread` creates a new thread each time it is called, which can have a significant performance overhead. Furthermore, it is possible to create more threads than the hardware can handle simultaneously, potentially resulting in a substantial slowdown.
|
||||
|
||||
This library contains a thread pool class, which avoids these issues by creating a fixed pool of threads once and for all, and then reusing the same threads to perform different tasks throughout the lifetime of the pool. By default, the number of threads in the pool is equal to the maximum number of threads that the hardware can run in parallel. The user submits tasks to be executed into a queue. Whenever a thread becomes available, it pops a task off the queue and executes it. If the task is a function with a return value, that value can be obtained later using an `std::future`.
|
||||
|
||||
In addition to `std::thread`, the C++ standard library also offers the higher-level construct `std::async`, which may internally utilize a thread pool - but this is not guaranteed, and in fact, currently only the MSVC implementation of `std::async` uses a thread pool, while GCC and Clang do not. Using a custom-made thread pool class instead of `std::async` allows the user more control, transparency, and portability.
|
||||
|
||||
<a id="markdown-features" name="features"></a>
|
||||
## Features
|
||||
|
||||
* **Fast:**
|
||||
* Built from scratch with performance in mind.
|
||||
* Compact code reduces both compilation time and binary size.
|
||||
* Reusing threads avoids the overhead of creating and destroying them.
|
||||
* A task queue ensures that there are never more threads running in parallel than allowed by the hardware.
|
||||
* **Lightweight:**
|
||||
* Only ~180 lines of code, excluding comments and blank lines.
|
||||
* Single header file: simply `#include "thread_pool.hpp"`, and you're done.
|
||||
* Header-only: no need to install or build the library.
|
||||
* Self-contained: no external requirements or dependencies. Does not require OpenMP or any other multithreading APIs. Only uses the C++ standard library, and works with any C++17-compliant compiler.
|
||||
* **Easy to use:**
|
||||
* Very simple operation, using a handful of member functions.
|
||||
* Futures are automatically generated for every task submitted to the queue.
|
||||
* The code is thoroughly documented using Doxygen comments - not only the interface, but also the implementation, in case you would like to make modifications.
|
||||
* **Additional features:**
|
||||
* Automatically parallelize a loop into any number of parallel tasks.
|
||||
* Synchronize output to a stream by multiple threads in parallel.
|
||||
|
||||
<a id="markdown-basic-usage" name="basic-usage"></a>
|
||||
## Basic usage
|
||||
|
||||
<a id="markdown-including-the-library" name="including-the-library"></a>
|
||||
### Including the library
|
||||
|
||||
To use the thread pool library, simply include the header file:
|
||||
|
||||
```cpp
|
||||
#include "thread_pool.hpp"
|
||||
```
|
||||
|
||||
The thread pool will now be accessible via the `thread_pool` class.
|
||||
|
||||
<a id="markdown-constructors" name="constructors"></a>
|
||||
### Constructors
|
||||
|
||||
The default constructor creates a thread pool with as many threads as the hardware can handle concurrently, as reported by the implementation via `std::thread::hardware_concurrency()`. With a hyperthreaded CPU, this will be twice the number of CPU cores. This is probably the constructor you want to use. For example:
|
||||
|
||||
```cpp
|
||||
// Constructs a thread pool with as many threads as available in the hardware.
|
||||
thread_pool pool;
|
||||
```
|
||||
|
||||
Optionally, a number of threads different from the hardware concurrency can be specified as an argument to the constructor. However, note that adding more threads than the hardware can handle will **not** improve performance, and in fact will most likely hinder it. This option exists in order to allow using **less** threads than the hardware concurrency, in cases where you wish to leave some threads available for other processes. For example:
|
||||
|
||||
```cpp
|
||||
// Constructs a thread pool with only 12 threads.
|
||||
thread_pool pool(12);
|
||||
```
|
||||
|
||||
<a id="markdown-submitting-tasks-to-the-queue" name="submitting-tasks-to-the-queue"></a>
|
||||
### Submitting tasks to the queue
|
||||
|
||||
A task can be any function, with zero or more arguments, and with or without a return value. Once a task has been submitted to the queue, it will be executed as soon as a thread becomes available. Tasks are executed in the order that they were submitted (first-in, first-out).
|
||||
|
||||
The member function `submit()` is used to submit tasks to the queue. The first argument is the function to execute, and the rest of the arguments are the arguments to pass to the function, if any. The return value is an `std::future` associated to the task. For example:
|
||||
|
||||
```cpp
|
||||
// Submit a task without arguments to the queue, and get a future for it.
|
||||
auto my_future = pool.submit(task);
|
||||
// Submit a task with one argument to the queue, and get a future for it.
|
||||
auto my_future = pool.submit(task, arg);
|
||||
// Submit a task with two arguments to the queue, and get a future for it.
|
||||
auto my_future = pool.submit(task, arg1, arg2);
|
||||
```
|
||||
|
||||
The value of the future depends on whether the function has a return value or not:
|
||||
|
||||
* If the submitted function has a return value, then the future will be set to that value when the function finishes its execution.
|
||||
* If the submitted function does not have a return value, then the future is a `bool` that will be set to `true` when the function finishes its execution.
|
||||
|
||||
To wait until the future's value becomes available, use the member function `wait()`. To obtain the value itself (and automatically wait for it if it is not yet ready), use the member function `get()`. For example:
|
||||
|
||||
```cpp
|
||||
// Submit a task and get a future.
|
||||
auto my_future = pool.submit(task);
|
||||
// Do some other stuff while the task is executing.
|
||||
do_stuff();
|
||||
// Get the task's return value from the future.
|
||||
auto my_return_value = my_future.get();
|
||||
```
|
||||
|
||||
Please see [the C++ reference entry for `std::future`](https://en.cppreference.com/w/cpp/thread/future) for more information about using futures.
|
||||
|
||||
<a id="markdown-parallelizing-loops" name="parallelizing-loops"></a>
|
||||
### Parallelizing loops
|
||||
|
||||
Consider the following loop:
|
||||
|
||||
```cpp
|
||||
for (T i = start; i <= end; i++)
|
||||
loop(i);
|
||||
```
|
||||
|
||||
where:
|
||||
|
||||
* `T` is any signed or unsigned integer type.
|
||||
* `start` is the first index to loop over (inclusive).
|
||||
* `end` is the last index to loop over (inclusive).
|
||||
* `loop()` is a function that takes exactly one argument, the loop index, and has no return value.
|
||||
|
||||
This loop may be automatically parallelized and submitted to the thread pool's queue using the `parallelize_loop()` member function as follows:
|
||||
|
||||
```cpp
|
||||
// Equivalent to the above loop, but will be automatically parallelized.
|
||||
pool.parallelize_loop(start, end, loop);
|
||||
```
|
||||
|
||||
The loop will be parallelized into a number of tasks equal to the number of threads in the pool, with each task executing the function `loop()` for a roughly equal number of indices. The calling thread will block and wait until all tasks generated by `parallelize_loop` (and only them) finish executing.
|
||||
|
||||
If desired, the number of parallel tasks may be manually specified using a fourth argument:
|
||||
|
||||
```cpp
|
||||
// Parallelize the loop into 12 parallel tasks
|
||||
pool.parallelize_loop(start, end, loop, 12);
|
||||
```
|
||||
|
||||
For best performance, it is recommended to do your own benchmarks to find the optimal number of tasks for each loop. Using less tasks than there are threads may be preferred if you are also running other tasks in parallel. Using more tasks than there are threads may improve performance in some cases (see the documentation for [my multithreaded matrix class template](https://github.com/bshoshany/multithreaded-matrix) for analysis).
|
||||
|
||||
<a id="markdown-advanced-usage" name="advanced-usage"></a>
|
||||
## Advanced usage
|
||||
|
||||
<a id="markdown-getting-and-resetting-the-number-of-threads-in-the-pool" name="getting-and-resetting-the-number-of-threads-in-the-pool"></a>
|
||||
### Getting and resetting the number of threads in the pool
|
||||
|
||||
The member function `get_thread_count()` returns the number of threads in the pool. This will be equal to `std::thread::hardware_concurrency()` if the default constructor has been used.
|
||||
|
||||
It is generally unnecessary to change the number of threads in the pool after it has been created, since the whole point of a thread pool is that you only create the threads once. However, if desired, this can be done using the `reset()` member function, which waits for all submitted tasks to be completed, then destroys all threads and creates a new thread pool with the given number of threads. As for the constructor, if no argument is given, the number of threads will be the hardware concurrency.
|
||||
|
||||
<a id="markdown-submitting-tasks-to-the-queue-without-futures" name="submitting-tasks-to-the-queue-without-futures"></a>
|
||||
### Submitting tasks to the queue without futures
|
||||
|
||||
Usually, it is best to submit a task to the queue using `submit()`. This allows you to wait for the task to finish and/or get its return value later. However, sometimes a future is not needed, for example when you just want to "set and forget" a certain task, or if the task already communicates with other tasks in other ways, without using futures. In such cases, you may wish to avoid the overhead involved in assigning the future to the task.
|
||||
|
||||
The member function `push_task()` allows you to submit a task to the queue without getting a future for it. The task can have any number of arguments, but it cannot have a return value. For example:
|
||||
|
||||
```cpp
|
||||
// Submit a task without arguments or return value to the queue.
|
||||
pool.push_task(task);
|
||||
// Submit a task with one argument and no return value to the queue.
|
||||
pool.push_task(task, arg);
|
||||
// Submit a task with two arguments and no return value to the queue.
|
||||
pool.push_task(task, arg1, arg2);
|
||||
```
|
||||
|
||||
<a id="markdown-manually-waiting-for-all-tasks-to-complete" name="manually-waiting-for-all-tasks-to-complete"></a>
|
||||
### Manually waiting for all tasks to complete
|
||||
|
||||
To wait for a **single** submitted task to complete, use `submit()` and then use the `wait()` or `get()` member functions of the obtained future. However, in cases where you need to wait until **all** submitted tasks finish their execution, or if the tasks have been submitted without futures using `push_task`, you may use the member function `wait_for_tasks()` of the thread pool.
|
||||
|
||||
Consider, for example, the following code:
|
||||
|
||||
```cpp
|
||||
thread_pool pool;
|
||||
size_t a[100];
|
||||
for (size_t i = 0; i < 100; i++)
|
||||
pool.push_task([&a, i] { a[i] = i * i; });
|
||||
std::cout << a[50];
|
||||
```
|
||||
|
||||
The output will most likely be garbage, since the task that modifies `a[50]` has not yet finished executing by the time we try to access that element (in fact, that task is probably still in the queue). One solution would be to use `submit` instead of `push_task`, but perhaps we don't want the overhead of waiting for 100 different futures. Instead, simply adding the line
|
||||
|
||||
```cpp
|
||||
pool.wait_for_tasks();
|
||||
```
|
||||
|
||||
after the `for` loop will ensure - as efficiently as possible - that all tasks have finished running before we attempt to access any elements of the array `a`, and the code will print out `2500` as expected. (Note, however, that `wait_for_tasks()` will wait for **all** tasks in the queue, including those that are unrelated to the `for` loop. Using `parallelize_loop()` would make much more sense in this case, as it will wait only for tasks related to the loop.)
|
||||
|
||||
<a id="markdown-synchronizing-printing-to-an-output-stream" name="synchronizing-printing-to-an-output-stream"></a>
|
||||
## Synchronizing printing to an output stream
|
||||
|
||||
<a id="markdown-motivation" name="motivation"></a>
|
||||
### Motivation
|
||||
|
||||
When printing to an output stream from multiple threads in parallel, the output may become garbled. For example, consider this code:
|
||||
|
||||
```cpp
|
||||
thread_pool pool;
|
||||
for (auto i = 1; i <= 5; i++)
|
||||
pool.push_task([i] {
|
||||
std::cout << "Task no. " << i << " executing.\n";
|
||||
});
|
||||
```
|
||||
|
||||
The output may look as follows:
|
||||
|
||||
```none
|
||||
Task no. Task no. 2Task no. 5 executing.
|
||||
Task no. executing.
|
||||
Task no. 1 executing.
|
||||
4 executing.
|
||||
3 executing.
|
||||
```
|
||||
|
||||
The reason is that, although each **individual** insertion to `std::cout` is thread-safe, there is no mechanism in place to ensure subsequent insertions from the same thread are printed contiguously.
|
||||
|
||||
<a id="markdown-the-synced-stream-class" name="the-synced-stream-class"></a>
|
||||
### The synced stream class
|
||||
|
||||
The class `synced_stream` is designed to avoid such synchronization issues. The constructor takes one optional argument, specifying the output stream to print to. If no argument is supplied, `std::cout` will be used:
|
||||
|
||||
```cpp
|
||||
// Construct a synced stream that will print to std::cout.
|
||||
synced_stream sync_out;
|
||||
// Construct a synced stream that will print to the output stream my_stream.
|
||||
synced_stream sync_out(my_stream);
|
||||
```
|
||||
|
||||
The member function `print()` takes an arbitrary number of arguments, which are inserted into the stream one by one, in the order they were given. `println()` does the same, but also prints a newline character `\n` at the end, for convenience. A mutex is used to synchronize this process, so that any other calls to `print()` or `println()` using the same `synced_stream` object must wait until the previous call has finished.
|
||||
|
||||
For example, this code:
|
||||
|
||||
```cpp
|
||||
synced_stream sync_out;
|
||||
thread_pool pool;
|
||||
for (auto i = 1; i <= 5; i++)
|
||||
pool.push_task([i, &sync_out] {
|
||||
sync_out.println("Task no. ", i, " executing.");
|
||||
});
|
||||
```
|
||||
|
||||
Will print out:
|
||||
|
||||
```none
|
||||
Task no. 1 executing.
|
||||
Task no. 2 executing.
|
||||
Task no. 3 executing.
|
||||
Task no. 4 executing.
|
||||
Task no. 5 executing.
|
||||
```
|
||||
|
||||
<a id="markdown-compiling" name="compiling"></a>
|
||||
## Compiling
|
||||
|
||||
This library was tested on the following compilers and platforms:
|
||||
|
||||
* GCC v10.2.0 on Windows 10 build 19042.746 and Ubuntu 20.04.1 LTS.
|
||||
* 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.
|
||||
|
||||
<a id="markdown-version-history" name="version-history"></a>
|
||||
## Version history
|
||||
|
||||
* Version 1.0 (2021-01-15)
|
||||
* Initial release.
|
||||
|
||||
<a id="markdown-feedback" name="feedback"></a>
|
||||
## Feedback
|
||||
|
||||
If you would like a request any additional features, or if you encounter any bugs, please feel free to open a new issue!
|
||||
|
||||
<a id="markdown-author-and-copyright" name="author-and-copyright"></a>
|
||||
## Author and copyright
|
||||
|
||||
Copyright (c) 2021 [Barak Shoshany](http://baraksh.com) (baraksh@gmail.com). Licensed under the [MIT license](LICENSE.txt).
|
||||
+366
@@ -0,0 +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 <algorithm> // std::max
|
||||
#include <atomic> // std::atomic
|
||||
#include <cstdint> // std::uint_fast32_t
|
||||
#include <functional> // std::function
|
||||
#include <future> // std::promise
|
||||
#include <iostream> // std::cout, std::ostream
|
||||
#include <memory> // std::shared_ptr, std::unique_ptr
|
||||
#include <mutex> // std::mutex, std::scoped_lock
|
||||
#include <queue> // std::queue
|
||||
#include <thread> // std::this_thread, std::thread
|
||||
#include <type_traits> // std::decay_t, std::enable_if_t, std::is_void_v, std::invoke_result_t
|
||||
#include <utility> // 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<ui32>(_thread_count, 1)), threads(new std::thread[std::max<ui32>(_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 <typename T, typename F>
|
||||
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<ui32> 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 <typename F>
|
||||
void push_task(const F &task)
|
||||
{
|
||||
tasks_waiting++;
|
||||
{
|
||||
const std::scoped_lock lock(queue_mutex);
|
||||
tasks.push(std::move(std::function<void()>(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<void()>, 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 <typename F, typename... A>
|
||||
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<ui32>(_thread_count, 1);
|
||||
threads.reset(new std::thread[std::max<ui32>(_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<bool> 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 <typename F, typename... A, typename = std::enable_if_t<std::is_void_v<std::invoke_result_t<std::decay_t<F>, std::decay_t<A>...>>>>
|
||||
std::future<bool> submit(const F &task, const A &... args)
|
||||
{
|
||||
std::shared_ptr<std::promise<bool>> promise(new std::promise<bool>);
|
||||
std::future<bool> 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 <typename F, typename... A, typename R = std::invoke_result_t<std::decay_t<F>, std::decay_t<A>...>, typename = std::enable_if_t<!std::is_void_v<R>>>
|
||||
std::future<R> submit(const F &task, const A &... args)
|
||||
{
|
||||
std::shared_ptr<std::promise<R>> promise(new std::promise<R>);
|
||||
std::future<R> 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<void()> &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<void()> 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<bool> 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<ui32> 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<std::function<void()>> 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<std::thread[]> 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 <typename... T>
|
||||
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 <typename... T>
|
||||
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;
|
||||
};
|
||||
Reference in New Issue
Block a user