diff --git a/.clang-format b/.clang-format
index 9d2d4ee..97d2458 100644
--- a/.clang-format
+++ b/.clang-format
@@ -1,65 +1,69 @@
---
-Language: Cpp
AccessModifierOffset: -4
-AlignAfterOpenBracket: Align
+AlignAfterOpenBracket: DontAlign
AlignArrayOfStructures: None
-AlignConsecutiveMacros: None
AlignConsecutiveAssignments: None
AlignConsecutiveBitFields: None
AlignConsecutiveDeclarations: None
-AlignEscapedNewlines: Right
-AlignOperands: Align
-AlignTrailingComments: true
-AllowAllArgumentsOnNextLine: true
-AllowAllParametersOfDeclarationOnNextLine: true
-AllowShortEnumsOnASingleLine: true
-AllowShortBlocksOnASingleLine: Never
+AlignConsecutiveMacros: None
+AlignConsecutiveShortCaseStatements:
+ Enabled: false
+AlignEscapedNewlines: Left
+AlignOperands: DontAlign
+AlignTrailingComments:
+ Kind: Always
+ OverEmptyLines: 1
+AllowAllArgumentsOnNextLine: false
+AllowAllParametersOfDeclarationOnNextLine: false
+AllowBreakBeforeNoexceptSpecifier: Never
+AllowShortBlocksOnASingleLine: Empty
AllowShortCaseLabelsOnASingleLine: false
+AllowShortCompoundRequirementOnASingleLine: true
+AllowShortEnumsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Empty
-AllowShortLambdasOnASingleLine: All
AllowShortIfStatementsOnASingleLine: Never
+AllowShortLambdasOnASingleLine: Empty
AllowShortLoopsOnASingleLine: false
-AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
-AlwaysBreakTemplateDeclarations: Yes
-AttributeMacros:
- - __capability
+AlwaysBreakTemplateDeclarations: "Yes"
+AttributeMacros: []
BinPackArguments: true
BinPackParameters: true
+BitFieldColonSpacing: Both
BraceWrapping:
- AfterCaseLabel: false
- AfterClass: false
- AfterControlStatement: Never
- AfterEnum: false
- AfterFunction: false
+ AfterCaseLabel: true
+ AfterClass: true
+ AfterControlStatement: Always
+ AfterEnum: true
+ AfterExternBlock: true
+ AfterFunction: true
AfterNamespace: false
- AfterObjCDeclaration: false
- AfterStruct: false
- AfterUnion: false
- AfterExternBlock: false
- BeforeCatch: false
- BeforeElse: false
- BeforeLambdaBody: false
+ AfterStruct: true
+ AfterUnion: true
+ BeforeCatch: true
+ BeforeElse: true
+ BeforeLambdaBody: true
BeforeWhile: false
IndentBraces: false
SplitEmptyFunction: true
- SplitEmptyRecord: true
SplitEmptyNamespace: true
+ SplitEmptyRecord: true
+BracedInitializerIndentWidth: 4
+BreakAdjacentStringLiterals: false
+BreakAfterAttributes: Never
BreakBeforeBinaryOperators: None
+BreakBeforeBraces: Custom
BreakBeforeConceptDeclarations: true
-BreakBeforeBraces: Allman
BreakBeforeInheritanceComma: false
-BreakInheritanceList: BeforeColon
BreakBeforeTernaryOperators: true
-BreakConstructorInitializersBeforeComma: false
BreakConstructorInitializers: BeforeColon
-BreakAfterJavaFieldAnnotations: false
+BreakConstructorInitializersBeforeComma: false
+BreakInheritanceList: BeforeColon
BreakStringLiterals: true
-ColumnLimit: 10000
-CommentPragmas: "^ IWYU pragma:"
-QualifierAlignment: Leave
+ColumnLimit: 1024
CompactNamespaces: false
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
@@ -69,72 +73,50 @@ DisableFormat: false
EmptyLineAfterAccessModifier: Never
EmptyLineBeforeAccessModifier: LogicalBlock
ExperimentalAutoDetectBinPacking: false
-PackConstructorInitializers: BinPack
-BasedOnStyle: ""
-ConstructorInitializerAllOnOneLineOrOnePerLine: false
-AllowAllConstructorInitializersOnNextLine: true
FixNamespaceComments: true
-ForEachMacros:
- - foreach
- - Q_FOREACH
- - BOOST_FOREACH
-IfMacros:
- - KJ_IF_MAYBE
IncludeBlocks: Preserve
-IncludeCategories:
- - Regex: '^"(llvm|llvm-c|clang|clang-c)/'
- Priority: 2
- SortPriority: 0
- CaseSensitive: false
- - Regex: '^(<|"(gtest|gmock|isl|json)/)'
- Priority: 3
- SortPriority: 0
- CaseSensitive: false
- - Regex: ".*"
- Priority: 1
- SortPriority: 0
- CaseSensitive: false
-IncludeIsMainRegex: "(Test)?$"
IncludeIsMainSourceRegex: ""
IndentAccessModifiers: false
-IndentCaseLabels: false
IndentCaseBlocks: false
+IndentCaseLabels: false
+IndentExternBlock: AfterExternBlock
IndentGotoLabels: true
IndentPPDirectives: None
-IndentExternBlock: AfterExternBlock
IndentRequires: false
IndentWidth: 4
IndentWrappedFunctionNames: false
-InsertTrailingCommas: None
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
KeepEmptyLinesAtTheStartOfBlocks: true
LambdaBodyIndentation: Signature
+Language: Cpp
MacroBlockBegin: ""
MacroBlockEnd: ""
MaxEmptyLinesToKeep: 1
-NamespaceIndentation: None
+NamespaceIndentation: Inner
ObjCBinPackProtocolList: Auto
ObjCBlockIndentWidth: 4
ObjCBreakBeforeNestedBlockParam: true
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
-PenaltyBreakAssignment: 2
-PenaltyBreakBeforeFirstCallParameter: 19
-PenaltyBreakComment: 300
-PenaltyBreakFirstLessLess: 120
-PenaltyBreakOpenParenthesis: 0
-PenaltyBreakString: 1000
-PenaltyBreakTemplateDeclaration: 10
-PenaltyExcessCharacter: 1000000
-PenaltyReturnTypeOnItsOwnLine: 60
-PenaltyIndentedWhitespace: 0
-PointerAlignment: Left
PPIndentWidth: -1
+PackConstructorInitializers: BinPack
+PenaltyBreakAssignment: 0
+PenaltyBreakBeforeFirstCallParameter: 0
+PenaltyBreakComment: 0
+PenaltyBreakFirstLessLess: 0
+PenaltyBreakOpenParenthesis: 0
+PenaltyBreakString: 0
+PenaltyBreakTemplateDeclaration: 0
+PenaltyExcessCharacter: 0
+PenaltyIndentedWhitespace: 0
+PenaltyReturnTypeOnItsOwnLine: 0
+PointerAlignment: Left
+QualifierAlignment: Leave
ReferenceAlignment: Pointer
ReflowComments: true
RemoveBracesLLVM: false
-SeparateDefinitionBlocks: Leave
+SeparateDefinitionBlocks: Always
ShortNamespaceLines: 1
SortIncludes: CaseSensitive
SortJavaStaticImport: Before
@@ -142,49 +124,36 @@ SortUsingDeclarations: true
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
+SpaceAroundPointerQualifiers: Default
SpaceBeforeAssignmentOperators: true
SpaceBeforeCaseColon: false
-SpaceBeforeCpp11BracedList: true
+SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeParensOptions:
AfterControlStatements: true
AfterForeachMacros: true
- AfterFunctionDefinitionName: false
AfterFunctionDeclarationName: false
+ AfterFunctionDefinitionName: false
AfterIfMacros: true
AfterOverloadedOperator: false
BeforeNonEmptyParentheses: false
-SpaceAroundPointerQualifiers: Default
SpaceBeforeRangeBasedForLoopColon: true
+SpaceBeforeSquareBrackets: false
SpaceInEmptyBlock: false
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: Never
+SpacesInCStyleCastParentheses: false
SpacesInConditionalStatement: false
SpacesInContainerLiterals: true
-SpacesInCStyleCastParentheses: false
SpacesInLineCommentPrefix:
+ Maximum: 1
Minimum: 1
- Maximum: -1
SpacesInParentheses: false
SpacesInSquareBrackets: false
-SpaceBeforeSquareBrackets: false
-BitFieldColonSpacing: Both
-Standard: Latest
-StatementAttributeLikeMacros:
- - Q_EMIT
-StatementMacros:
- - Q_UNUSED
- - QT_REQUIRE_VERSION
+Standard: c++17
TabWidth: 4
UseCRLF: false
UseTab: Never
-WhitespaceSensitiveMacros:
- - STRINGIZE
- - PP_STRINGIZE
- - BOOST_PP_STRINGIZE
- - NS_SWIFT_NAME
- - CF_SWIFT_NAME
----
diff --git a/.clang-tidy b/.clang-tidy
new file mode 100644
index 0000000..74d9700
--- /dev/null
+++ b/.clang-tidy
@@ -0,0 +1,45 @@
+---
+CheckOptions:
+ misc-const-correctness.WarnPointersAsValues: true
+ misc-include-cleaner.IgnoreHeaders: time.h
+Checks: >
+ -*,
+ bugprone-*,
+ -bugprone-easily-swappable-parameters,
+ -bugprone-empty-catch,
+ -bugprone-exception-escape,
+ cert-*,
+ -cert-err58-cpp,
+ concurrency-*,
+ -concurrency-mt-unsafe,
+ cppcoreguidelines-*,
+ -cppcoreguidelines-avoid-c-arrays,
+ -cppcoreguidelines-avoid-do-while,
+ -cppcoreguidelines-avoid-non-const-global-variables,
+ -cppcoreguidelines-interfaces-global-init,
+ -cppcoreguidelines-macro-usage,
+ -cppcoreguidelines-missing-std-forward,
+ -cppcoreguidelines-pro-bounds-array-to-pointer-decay,
+ -cppcoreguidelines-special-member-functions,
+ hicpp-*,
+ -hicpp-avoid-c-arrays,
+ -hicpp-braces-around-statements,
+ -hicpp-named-parameter,
+ -hicpp-no-array-decay,
+ -hicpp-special-member-functions,
+ -hicpp-use-auto,
+ misc-*,
+ -misc-definitions-in-headers,
+ modernize-*,
+ -modernize-avoid-c-arrays,
+ -modernize-use-auto,
+ -modernize-use-trailing-return-type,
+ performance-*,
+ portability-*,
+ readability-*,
+ -readability-braces-around-statements,
+ -readability-const-return-type,
+ -readability-named-parameter,
+ -readability-redundant-member-init,
+ -readability-use-anyofallof,
+HeaderFilterRegex: .*
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index bcbacbf..14d4b0e 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -10,6 +10,10 @@ What does your pull request fix or add to the library?
Have you formatted your code using the `.clang-format` file attached to this project?
+**Linting**
+
+Have you linted your code using the `.clang-tidy` file attached to this project?
+
**Testing**
Have you tested the new code using the provided automated test program `BS_thread_pool_test.cpp` (preferably with the provided multi-compiler test script `BS_thread_pool_test.ps1`) and/or performed any other tests to ensure that the new code works correctly?
diff --git a/.gitignore b/.gitignore
index 378eac2..c6adf5c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
build
+temp
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bfa2cdf..ca59296 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ Website: \
GitHub:
* [Version history](#version-history)
+ * [v4.0.0 (2023-12-27)](#v400-2023-12-27)
* [v3.5.0 (2023-05-25)](#v350-2023-05-25)
* [v3.4.0 (2023-05-12)](#v340-2023-05-12)
* [v3.3.0 (2022-08-03)](#v330-2022-08-03)
@@ -26,11 +27,146 @@ GitHub:
## Version history
+### v4.0.0 (2023-12-27)
+
+* A major new release with numerous changes, additions, fixes, and improvements. Many frequently requested features have been added, and performance has been optimized. Please note that code written using previous releases will need to be modified to work with the new release. The changes needed to migrate to the new API are explicitly indicated below for your convenience.
+* Highlights:
+ * The light thread pool has been removed. However, by default, the thread pool is in "light mode". Optional features that may affect performance must be enabled by defining suitable macros.
+ * This library now ships with two stand-alone header files:
+ * `BS_thread_pool.hpp` contains the main `BS::thread_pool` class and the `BS::multi_future` helper classes, and is the only file needed to use the thread pool itself.
+ * `BS_thread_pool_utils.hpp` contains the additional utility classes `BS::signaller`, `BS::synced_stream`, and `BS::timer`, which are fully independent of the thread pool itself and can be used either with or without it.
+ * It is now possible to assign priorities to tasks. Tasks with higher priorities will be executed first.
+ * Member functions for submitting tasks and loops have been renamed for consistency, e.g. `detach_task()` and `submit_task()`, where the prefix `detach` means no future will be returned and `submit` means a future or `BS::multi_future` will be returned.
+ * There are now two ways to parallelize loops into blocks:
+ * `detach_blocks()` and `submit_blocks()` behave the same as loop parallelization in previous releases, running the loop function once per block.
+ * `detach_loop()` and `submit_loop()` have a simpler syntax, where the loop function is run once per index, so the user doesn't have to manually run the internal loop for each block.
+ * The new member functions `detach_sequence()` and `submit_sequence()` allow submitting a sequence of tasks enumerated by indices.
+ * It is now possible to run an initialization function in each thread before it starts to execute any submitted tasks.
+ * Tasks submitted with `detach_task()` or `submit_task()` can no longer have arguments. Task with arguments must be enclosed inside lambda expressions. This simplifies the API and provides better readability. Tasks can still have return values.
+ * Various ways to obtain information about the threads in the pool have been introduced:
+ * The member function `get_thread_ids()` obtains the unique thread identifiers, and `get_native_handles()` obtains the underlying implementation-defined thread handles.
+ * The new namespace `BS::this_thread` allows obtaining the thread's index in the pool using `BS::this_thread::get_index()` and a pointer to the pool that owns the thread using `BS::this_thread::get_pool()`.
+ * Member functions for waiting for tasks have been renamed for brevity: `wait()`/`wait_for()`/`wait_until()`. In addition, these functions can now optionally throw an exception if the user tries to call them from within a thread of the same pool, which would result in a deadlock.
+ * The first index must now be specified explicitly when parallelizing blocks, loops, and sequences, and it must not be greater than the last index. Also, both indices must now have the same type, or the template parameter should be explicitly specified.
+ * Optimized the way `detach_blocks()`, `submit_blocks()`, `detach_loop()`, and `submit_loop()` split the range of the loop into blocks.
+ * Added a utility class `BS::signaller` to allow simple signalling between threads.
+ * `BS::multi_future` is now a specialization of `std::vector>` with additional member functions.
+* Breaking changes:
+ * The light thread pool has been removed. The original idea was that the light thread pool will allow the user to sacrifice functionality for increased performance. However, in my testing I found that there was no actual performance benefit to the light thread pool. Therefore, there is no reason to keep it.
+ * However, by default, the thread pool is in "light mode". Optional features that may affect performance due to additional checks or more complicated algorithms must be enabled by defining suitable macros before including the library:
+ * `BS_THREAD_POOL_ENABLE_PAUSE` to enable pausing.
+ * `BS_THREAD_POOL_ENABLE_PRIORITY` to enable task priority.
+ * `BS_THREAD_POOL_ENABLE_WAIT_DEADLOCK_CHECK` to enable wait deadlock checks.
+ * **API migration:**
+ * If you previously used `BS_thread_pool_light.hpp`, simply use `BS_thread_pool.hpp` instead.
+ * If you previously used the pausing feature, define the macro `BS_THREAD_POOL_ENABLE_PAUSE` before including `BS_thread_pool.hpp` to enable it.
+ * Member functions have been renamed for better consistency. Each function has a `detach` variant which does not return a future, and a `submit` variant which does return a future (or a `BS::multi_future`):
+ * `detach_task()` and `submit_task()` for single tasks.
+ * `detach_blocks()` and `submit_blocks()` for loops to be split into blocks, where the loop function is executed once per block and must have an internal loop, as in previous releases.
+ * `detach_loop()` and `submit_loop()` for loops to be split into blocks, where the loop function is executed once per index and the pool takes care of the internal loop.
+ * `detach_sequence()` and `submit_sequence()` for sequences of enumerated tasks.
+ * **API migration:** Use the new names of the functions:
+ * `push_task()` -> `detach_task()`
+ * `submit()` -> `submit_task()`
+ * `push_loop()` -> `detach_blocks()`
+ * `parallelize_loop()` -> `submit_blocks()`
+ * `wait_for_tasks()`, `wait_for_tasks_duration()`, and `wait_for_tasks_until()` have been renamed to `wait()`, `wait_for()`, and `wait_until()` respectively.
+ * **API migration:** Use the new names of the functions:
+ * `wait_for_tasks()` -> `wait()`
+ * `wait_for_tasks_duration()` -> `wait_for()`
+ * `wait_for_tasks_until()` -> `wait_until()`
+ * Functions for parallelizing loops no longer have dedicated overloads for the special case where the first index is 0. These overloads essentially amount to giving the first function argument a default value, which is not allowed in C++, and can be confusing. In addition, indicating the first index explicitly is better for readability.
+ * **API migration:** Add the first index 0 manually as the first argument if it was omitted.
+ * Functions for parallelizing loops no longer allow the last index to be smaller than the first index. Previously, e.g. `detach_blocks(5, 0, ...)` was equivalent to `detach_blocks(0, 5, ...)`. However, this led to confusing results. Since the first argument is the first index and the second argument is the index *after* the last index (i.e. 0 to 5 actually means 0, 1, 2, 3, 4), the user might get the wrong impression that `detach_blocks(5, 0, ...)` will count 5, 4, 3, 2, 1 instead. This option was removed to avoid this confusion.
+ * Sometimes the user might actually want to make a loop that counts down instead of up. This cannot be done by flipping the order of the arguments to e.g. `detach_blocks()` (nor could it be done in previous releases). However, it can be done by simply defining a suitable loop function. For example, if you call `detach_blocks(0, 10, loop, 2)` and define the loop function as `for (T i = 9 - start; i > 9 - end; --i)`, then the first block will count 9, 8, 7, 6, 5 and the second block will count 4, 3, 2, 1, 0.
+ * `detach_loop()`, `submit_loop()`, `detach_sequence()`, and `submit_sequence()` work the same way. The first index must be smaller than the last index, but you can count down by writing a suitable loop or sequence function.
+ * **API migration:** Any loop parallelization that used a first index greater than the last index will work exactly the same after switching the first and second arguments so that the smaller index appears first.
+ * Functions for parallelizing loops no longer accept first and last indices of different types. The reason for allowing this previously was that otherwise, writing something like `detach_blocks(0, x, ...)` where `x` is not an `int` would result in a compilation error, since `0` is by default an `int` and therefore the arguments `0` and `x` have different types. However, this behavior, which used [`std::common_type`](https://en.cppreference.com/w/cpp/types/common_type) to determine the common type of the two indices, sometimes completely messed up the range of the loop. For example, the `std::common_type` of `int` and `unsigned int` is `unsigned int`, which means the loop will only use non-negative indices even if the `int` start index was negative, resulting in an integer overflow.
+ * **API migration:** If you want to invoke e.g. `detach_blocks(0, x, ...)` where `x` is not an `int`, you can either:
+ * Make the `0` have the desired type using a cast or a suffix. For example, if `x` is an `unsigned int`, write `(unsigned int)0` or `0U` instead of `0`.
+ * Specify the template parameter explicitly. For example, if `x` is a `size_t`, write `detach_blocks(0, x, ...)`.
+ * `detach_task()` and `submit_task()` no longer accept arguments for the submitted task. Instead, you must enclose the function in a [lambda expression](https://en.cppreference.com/w/cpp/language/lambda). In other words, instead of `detach_task(task, args...)` you should write `detach_task([] { task(args...); })`, indicating in the capture list `[]` whether to capture the task itself, and each of the arguments, by value or reference. Please see `README.md` for examples. This was changed for the following reasons:
+ 1. Consistency with `detach_blocks()` and `submit_blocks()`, as well as the new `detach_loop()`, `submit_loop()`, `detach_sequence()`, and `submit_sequence()`, which do not accept function arguments either.
+ 2. In my own multithreaded projects, I find that I almost always need the task to have access to variables in the local scope. This is much simpler, easier, and more concise to do with a lambda capture list, especially an implicit capture `[=]` or `[&]`, than by defining a function that takes arguments and then passing these arguments.
+ 3. Similarly, I find that I mostly submit tasks defined as a lambda on the spot, rather than creating them as separate functions, because it's faster to code and makes it clear exactly what the task does without having to look elsewhere.
+ 4. When users post issues to this repository asking for help with their own code that uses the thread pool, the solution often turns out to be "just wrap that in a lambda". Such issues can be avoided if lambdas must be used to begin with.
+ 5. Submitting member functions, which previously required the awkward syntax `detach_task(&class::function, &object, args...)`, can now be achieved with the much simpler and more readable syntax `detach_task([] { object.function(args...); })` with the appropriate captures.
+ 6. Passing arguments by reference, which previously required using [`std::ref`](https://en.cppreference.com/w/cpp/utility/functional/ref), e.g. `detach_task(task, std::ref(arg))`, can now be achieved with the much simpler and more readable syntax `detach_task([&arg] { task(arg); })`.
+ 7. The new syntax allows specifying the priority of the task easily, as the second argument - otherwise, it would have been hard to distinguish the priority from a task argument, making the API more complicated and confusing. This syntax will also permit adding additional arguments to the member functions as needed in the future.
+ * **API migration:** Enclose all tasks with arguments inside a lambda expression. All submitted tasks must have no arguments, but they can still have return values.
+ * Alternatively, [`std::bind`](https://en.cppreference.com/w/cpp/utility/functional/bind) can also be used, if the old syntax is preferred to a lambda. Just wrap it around the task and its arguments: instead of `detach_task(task, args...)`, write `detach_task(std::bind(task, args...))`. This achieves the same effect, and can be used to easily convert v3.x.x code to v4.0.0 using a simple regular expression search and replace:
+ * `push_task\((.*?)\)` -> `detach_task(std::bind($1))`
+ * `submit\((.*?)\)` -> `submit_task(std::bind($1))`
+ * `BS::synced_stream` and `BS::timer` have been moved to `BS_thread_pool_utils.hpp`.
+ * **API migration:** Include the new header file if either of these utility classes are used.
+* `BS_thread_pool.hpp` new features:
+ * A new optional feature, enabled by defining the macro `BS_THREAD_POOL_ENABLE_PRIORITY`, allows assigning priority to tasks. The priority is a number of type `BS::priority_t`, which is a signed 16-bit integer, so it can have any value between -32,768 and 32,767. The tasks will be executed in priority order from highest to lowest.
+ * To assign a priority to a task, add the priority as the last argument to any of the `detach` or `submit` functions. If the priority is not specified, the default value will be 0.
+ * The namespace `BS::pr` contains some pre-defined priorities for users who wish to avoid magic numbers and enjoy better future-proofing. In order of decreasing priority, the pre-defined priorities are: `BS::pr::highest`, `BS::pr::high`, `BS::pr::normal`, `BS::pr::low`, and `BS::pr::lowest`.
+ * Please see `README.md` for more information, including performance considerations.
+ * The new member functions `detach_loop()` and `submit_loop()` facilitate loop parallelization without having to worry about internal loops in the loop function. In previous releases, the loop function had to be of the form `[](T start, T end) { for (T i = start; i < end; ++i) loop(i); }`. This behavior has been preserved in `detach_blocks()` and `submit_blocks()`. However, the new `detach_loop()` and `submit_loop()` allow much simpler loop functions of the form `[](T i) { loop(i) }`, greatly simplifying the interface.
+ * Performance-wise, due to fewer function calls, `detach_blocks()` and `submit_blocks()` are generally faster. However, the difference is usually not significant, and with compiler optimizations there may be no difference at all. In any case, `detach_loop()` and `submit_loop()` are provided as convenience functions, but performance-critical applications can stick with `detach_blocks()` and `submit_blocks()`.
+ * The new member functions `detach_sequence()` and `submit_sequence()` facilitate submitting a sequence of tasks enumerated by indices. This is a bit similar to `detach_loop()` and `submit_loop()`, except that the range of indices is not split into blocks with each block containing a smaller range of indices. Instead, there is exactly one task per index. This can be used, for example, to submit a sequence of tasks with each one independently processing a single array element. `detach_sequence()` does not return a future, while `submit_sequence()` returns a `BS::multi_future`.
+ * It is now possible to run an initialization function in each thread before it starts to execute any submitted tasks. The function must take no arguments and have no return value. It will only be executed exactly once, when the thread is first constructed. It can be passed as an argument to the constructor or to `reset()`. See [#104](https://github.com/bshoshany/thread-pool/issues/104), [#105](https://github.com/bshoshany/thread-pool/pull/105), [#113](https://github.com/bshoshany/thread-pool/issues/113), and [#119](https://github.com/bshoshany/thread-pool/issues/119).
+ * Added a member function `get_thread_ids()` which returns a vector containing the unique identifiers for each of the pool's threads, as obtained by [`std::thread::get_id()`](https://en.cppreference.com/w/cpp/thread/get_id). See [#126](https://github.com/bshoshany/thread-pool/issues/126).
+ * A new optional feature, enabled by defining the macro `BS_THREAD_POOL_ENABLE_NATIVE_HANDLES`, adds a member function `get_native_handles()` which returns a vector containing the underlying implementation-defined thread handles for each of the pool's threads. These can then be used in an implementation-specific way to manage the threads at the OS level; however, note that this will generally **not** be portable code. See [#122](https://github.com/bshoshany/thread-pool/issues/122).
+ * This feature is disabled by default since it uses [std::thread::native_handle()](https://en.cppreference.com/w/cpp/thread/thread/native_handle), which is in the C++ standard library, but is **not** guaranteed to be present on all systems.
+ * A new namespace `BS::this_thread` was created to provide functionality similar to `std::this_thread`.
+ * `BS::this_thread::get_index()` can be used to get the index of the current thread. If this thread belongs to a `BS::thread_pool` object, it will have an index from 0 to `BS::thread_pool::get_thread_count() - 1`. Otherwise, for example if this thread is the main thread or an independent [`std::thread`](https://en.cppreference.com/w/cpp/thread/thread), [`std::nullopt`](https://en.cppreference.com/w/cpp/utility/optional/nullopt) will be returned.
+ * `BS::this_thread::get_pool()` can be used to get the pointer to the thread pool that owns the current thread. If this thread belongs to a `BS::thread_pool` object, a pointer to that object will be returned. Otherwise, `std::nullopt` will be returned.
+ * Note that both functions return an [`std::optional`](https://en.cppreference.com/w/cpp/utility/optional) object.
+ * `BS::multi_future` is now defined as a specialization of `std::vector>`. This means that all of the member functions that can be used on an [`std::vector`](https://en.cppreference.com/w/cpp/container/vector) can also be used on a `BS::multi_future`. For example, it is now possible to use a range-based `for` loop with a `BS::multi_future` object, since it has iterators.
+ * In addition to inherited member functions, `BS::multi_future` has the following specialized member functions, most of which are new in this release: `get()`, `ready_count()`, `valid()`, `wait()`, `wait_for()`, and `wait_until()`. Please see `README.md` for more information. See also [#128](https://github.com/bshoshany/thread-pool/issues/128).
+ * A new optional feature, enabled by defining the macro `BS_THREAD_POOL_ENABLE_WAIT_DEADLOCK_CHECK`, allows `wait()`, `wait_for()`, and `wait_until()` to check whether the user tried to call them from within a thread of the same pool, which would result in a deadlock. If so, they will throw the exception `BS::thread_pool::wait_deadlock` instead of waiting.
+* `BS_thread_pool_utils.hpp`:
+ * The utility classes `BS::synced_stream` and `BS::timer` now reside in this header file instead of the main one.
+ * `BS::timer` has a new member function, `current_ms()`, which can be used to obtain the number of milliseconds that have elapsed so far, but keep the timer ticking.
+ * The new utility class `BS::signaller` allows simple signalling between threads. It can be used to make one or more threads wait, using the `wait()` member function. When another thread uses the `ready()` member function, all waiting threads stop waiting. This class is really just a convenient wrapper around [`std::promise`](https://en.cppreference.com/w/cpp/thread/promise), which contains both the promise and its future.
+* `BS_thread_pool.hpp` bug fixes and minor changes:
+ * Optimized locking in the worker function. This should result in increased performance.
+ * Optimized the way `detach_blocks()`, `submit_blocks()`, `detach_loop()`, and `submit_loop()` split the range of the loop into blocks. All blocks are now guaranteed to have one of two sizes, differing by 1, with the larger blocks always first. See [#96](https://github.com/bshoshany/thread-pool/issues/96).
+ * For example, in previous releases, 100 indices were split into 15 blocks as 14 blocks of size 6 and one additional block of size 16, which was suboptimal. Now they are split into 10 blocks of size 7 and 5 blocks of size 6, which means the tasks are as evenly distributed as possible.
+ * Fixed a bug that caused paused pools to have high idle CPU usage if pausing was used. See [#120](https://github.com/bshoshany/thread-pool/issues/120).
+ * The worker now destructs the task object as soon as it finishes executing. See [#124](https://github.com/bshoshany/thread-pool/issues/124) and [#129](https://github.com/bshoshany/thread-pool/pull/129).
+ * Added Markdown inline code formatting in all comments whenever applicable, which makes the comments look nicer when displayed as a tooltip in [Visual Studio Code](https://code.visualstudio.com/) or other supporting IDEs.
+ * The `BS::thread_pool::blocks` helper class has been moved into the main thread pool class, and now returns a degenerate object (zero blocks) if `index_after_last <= first_index`.
+* `BS_thread_pool_test.cpp`:
+ * Removed tests for the light thread pool.
+ * Added/modified tests for all new/changed features.
+ * Many of the previous tests have been simplified and optimized.
+ * The program now takes command line arguments:
+ * `help`: Show a help message and exit.
+ * `log`: Create a log file.
+ * `tests`: Perform standard tests.
+ * `deadlock` Perform long deadlock tests.
+ * `benchmarks`: Perform benchmarks.
+ * If no options are entered, the default is: `log tests benchmarks`.
+ * By default, the test program enables all the optional features by defining the suitable macros, so it can test them. However, if the macro `BS_THREAD_POOL_LIGHT_TEST` is defined during compilation, the optional features will not be tested.
+ * Instead of using a pre-defined list to specify the number of loop blocks to try in the benchmarks, the program now simply keeps increasing the number of blocks until it finds the optimal value. Often, the optimal number of blocks is much higher than the number of hardware threads, but if the number is too high it will result in diminishing returns.
+ * `check_loop_no_return()` now checks that the loop modifies all the indices exactly once, to detect cases where an index has been modified more than once, e.g. if the same loop index was erroneously placed in more than one block.
+ * Instead of defining `_CRT_SECURE_NO_WARNINGS`, the program now uses [`localtime_s`](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/localtime-s-localtime32-s-localtime64-s) instead of [`std::localtime`](https://en.cppreference.com/w/cpp/chrono/c/localtime) if MSVC is detected to avoid generating a warning.
+ * On macOS, the test program will exit with [`std::terminate()`](https://en.cppreference.com/w/cpp/error/terminate) instead of [`std::quick_exit()`](https://en.cppreference.com/w/cpp/utility/program/quick_exit) if any tests failed. This is because macOS does not implement `std::quick_exit()` for some reason. Note that as a result, the number of failed tests cannot be returned by the program on macOS. Unfortunately, [`std::exit()`](https://en.cppreference.com/w/cpp/utility/program/exit) cannot be used here, as it might get stuck if a deadlock occurs. See [#106](https://github.com/bshoshany/thread-pool/pull/106)
+ * The log file now uses the name of the executable file, followed by the date and time, so it's easy to distinguish between log files generated by different builds of the test (since the test script names them based on the compiler used). Also, the program now checks if the log file failed to open for some reason, and writes only to the standard output in that case.
+ * The benchmarks now display a progress bar.
+ * The test program will now detect the OS and compiler used.
+* `BS_thread_pool_test.ps1`:
+ * The script will compile and run a light version of the test, with no optional features enabled, in addition to the main test, for each compiler.
+ * The source and build folders will now be determined relative to the script folder, to ensure that the script works no matter which folder it is executed from.
+ * The script now checks that the include files `BS_thread_pool.hpp` and `BS_thread_pool_utils.hpp` are present before attempting to compile the test program.
+* `README.md`:
+ * Added/modified documentation for all new/changed features.
+ * Revised many of the existing examples and explanations.
+ * Added a complete library reference at the end of the documentation.
+ * Added instructions for installing the package using Meson and CMake with CPM. The installation instructions with various package managers and build systems were moved to the end, before the reference.
+* Miscellaneous:
+ * A `.clang-tidy` file is now included, with all the checks that are enabled in this project. The pull request template has been updated to suggest that authors lint their code using this file before submitting the pull request.
+* This release is dedicated to my wife (since December 1, 2023), Pauline. Her endless love, support, and encouragement have been a great source of motivation for working on this and other projects. I am so lucky and honored to [`my_future.share()`](https://en.cppreference.com/w/cpp/thread/future/share) with her ❤️
+
### v3.5.0 (2023-05-25)
* `BS_thread_pool.hpp` and `BS_thread_pool_light.hpp`:
* Added a new member function, `purge()`, to the full (non-light) thread pool. This function purges all the tasks waiting in the queue. Tasks that are currently running will not be affected, but any tasks still waiting in the queue will be removed and will never be executed by the threads. Please note that there is no way to restore the purged tasks.
- * Fix a bug which caused `wait_for_tasks()` to only block the first thread that called it. Now it blocks every thread that calls it, which is the expected behavior. In addition, all related deadlock have now been completely resolved. This also applies to the variants `wait_for_tasks_duration()` and `wait_for_tasks_until()` in the non-light version. See [#110](https://github.com/bshoshany/thread-pool/pull/110).
+ * Fixed a bug which caused `wait_for_tasks()` to only block the first thread that called it. Now it blocks every thread that calls it, which is the expected behavior. In addition, all related deadlock have now been completely resolved. This also applies to the variants `wait_for_tasks_duration()` and `wait_for_tasks_until()` in the non-light version. See [#110](https://github.com/bshoshany/thread-pool/pull/110).
* Note: You should never call `wait_for_tasks()` from within a thread of the same thread pool, as that will cause it to wait forever! This fix is relevant for situations when `wait_for_tasks()` is called from an auxiliary `std::thread` or a separate thread pool.
* `push_task()` and `submit()` now avoid creating unnecessary copies of the function object. This should improve performance, especially if large objects are involved. See [#90](https://github.com/bshoshany/thread-pool/pull/90).
* Optimized the way condition variables are used by the thread pool class. Shared variables are now modified while owning the mutex, but condition variables are notified after the mutex is released, if possible. See [#84](https://github.com/bshoshany/thread-pool/pull/84).
@@ -65,7 +201,7 @@ GitHub:
* Two new member functions have been added to the non-light version: `wait_for_tasks_duration()` and `wait_for_tasks_until()`. They allow waiting for the tasks to complete, but with a timeout. `wait_for_tasks_duration()` will stop waiting after the specified duration has passed, and `wait_for_tasks_until()` will stop waiting after the specified time point has been reached.
* Renamed `BS_THREAD_POOL_VERSION` in `BS_thread_pool_light.hpp` to `BS_THREAD_POOL_LIGHT_VERSION` and removed the `[light]` tag. This allows including both header files in the same program in case we want to use both the light and non-light thread pools simultaneously.
* `BS_thread_pool_test.cpp` and `BS_thread_pool_light_test.cpp`:
- * Fixed an issue that caused a compilation error when using MSVC and including `Windows.h`. See [#72](https://github.com/bshoshany/thread-pool/pull/72).
+ * Fixed an issue that caused a compilation error when using MSVC and including `windows.h`. See [#72](https://github.com/bshoshany/thread-pool/pull/72).
* The number and size of the vectors in the performance test (`BS_thread_pool_test.cpp` only) are now guaranteed to be multiples of the number of threads, for optimal performance.
* In `count_unique_threads()`, moved the condition variables and mutexes to the function scope to prevent cluttering the global scope.
* Three new tests have been added to `BS_thread_pool_test.cpp` to check the deadlocks issue that were resolved in this release (see above). The tests rely on the new wait for tasks with timeout feature, so they are not available in the light version.
@@ -117,7 +253,7 @@ GitHub:
* Instead of generating random vectors using `std::mersenne_twister_engine`, which proved to be inconsistent across different compilers and systems, the test now generates each element via an arbitrarily-chosen numerical operation. In my testing, this provided much more consistent results.
* Instead of using a hard-coded vector size, a suitable vector size is now determined dynamically at runtime.
* Instead of using `parallelize_loop()`, the test now uses the new `push_loop()` function to squeeze out a bit more performance.
- * Instead of setting the test parameters to achieve a fixed single-threaded mean execution time of 300 ms, the test now aims to achieve a fixed multi-threaded mean execution time of 50 ms when the number of blocks is equal to the number of threads. This allows for more reliable results on very fast CPUs with a very large number of threads, where the mean execution time when using all the threads could previously be below a statistically significant value.
+ * Instead of setting the test parameters to achieve a fixed single-threaded mean execution time of 300 ms, the test now aims to achieve a fixed multithreaded mean execution time of 50 ms when the number of blocks is equal to the number of threads. This allows for more reliable results on very fast CPUs with a very large number of threads, where the mean execution time when using all the threads could previously be below a statistically significant value.
* The number of vectors is now restricted to be a multiple of the number of threads, so that the blocks are always all of the same size.
* `README.md`:
* Added instructions and examples for the new features described above.
@@ -129,7 +265,7 @@ GitHub:
* Fixed an issue where `wait_for_tasks()` would sometimes get stuck if `push_task()` was executed immediately before `wait_for_tasks()`.
* Both the thread pool constructor and the `reset()` member function now determine the number of threads to use in the pool as follows. If the parameter is a positive number, then the pool will be created with this number of threads. If the parameter is non-positive, or a parameter was not supplied, then the pool will be created with the total number of hardware threads available, as obtained from `std::thread::hardware_concurrency()`. If the latter returns a non-positive number for some reason, then the pool will be created with just one thread. See [#51](https://github.com/bshoshany/thread-pool/issues/51) and [#52](https://github.com/bshoshany/thread-pool/issues/52).
* Added the `[[nodiscard]]` attribute to classes and class members, in order to warn the user when accidentally discarding an important return value, such as a future or the return value of a function with no useful side-effects. For example, if you use `submit()` and don't save the future it returns, the compiler will now generate a warning. (If a future is not needed, then you should use `push_task()` instead.)
- * Removed the `explicit` specifier from all constructors, as it prevented the default constructor from being used with static class members. See [#48](https://github.com/bshoshany/thread-pool/issues/48>).
+ * Removed the `explicit` specifier from all constructors, as it prevented the default constructor from being used with static class members. See [#48](https://github.com/bshoshany/thread-pool/issues/48).
* `BS_thread_pool_test.cpp`:
* Improved `count_unique_threads()` using condition variables, to ensure that each thread in the pool runs at least one task regardless of how fast it takes to run the tasks.
* When appropriate, `check()` now explicitly reports what the obtained result was and what it was expected to be.
@@ -174,8 +310,8 @@ GitHub:
* Added a new test for `parallelize_loop()` with a return value.
* Improved some of the tests to make them more reliable. For example, `count_unique_threads()` now uses futures (stored in a `BS::multi_future` object).
* The program now uses `std::vector` instead of matrices, for both consistency checks and benchmarks, in order to simplify the code and considerably reduce its length.
- * The benchmarks have been simplified. There's now only one test: filling a specific number of vectors of fixed size with random values. This may be replaced with something more practical in a future released, but at least on the systems I've tested on, it does demonstrate a very significant multi-threading speedup.
- * In addition to multi-threaded tests with different numbers of tasks, the benchmark now also includes a single-threaded test. This allows for more accurate benchmarks compared to previous versions, as the (slight) parallelization overhead is now taken into account when calculating the maximum speedup.
+ * The benchmarks have been simplified. There's now only one test: filling a specific number of vectors of fixed size with random values. This may be replaced with something more practical in a future released, but at least on the systems I've tested on, it does demonstrate a very significant multithreading speedup.
+ * In addition to multithreaded tests with different numbers of tasks, the benchmark now also includes a single-threaded test. This allows for more accurate benchmarks compared to previous versions, as the (slight) parallelization overhead is now taken into account when calculating the maximum speedup.
* The program decides how many vectors to use for benchmarking by testing how many are needed to reach a target duration in the single-threaded test. This ensures that the test takes approximately the same amount of time on different systems, and is thus more consistent and portable.
* Miscellaneous minor optimizations and style improvements.
* Changes to `README.md`:
diff --git a/CITATION.cff b/CITATION.cff
index 99bb729..b64f76f 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -1,24 +1,26 @@
---
authors:
- - family-names: "Shoshany"
- given-names: "Barak"
- orcid: "https://orcid.org/0000-0003-2222-127X"
-cff-version: "1.2.0"
+ -
+ family-names: Shoshany
+ given-names: Barak
+ orcid: https://orcid.org/0000-0003-2222-127X
+cff-version: 1.2.0
date-released: "2021-05-03"
-doi: "10.5281/zenodo.4742687"
-license: "MIT"
-message: "If you use this package in published research, please cite it as follows."
-repository-code: "https://github.com/bshoshany/thread-pool"
-title: "A C++17 Thread Pool for High-Performance Scientific Computing"
+doi: 10.5281/zenodo.4742687
+license: MIT
+message: If you use this library in published research, please cite it as follows.
preferred-citation:
- type: "article"
authors:
- - family-names: "Shoshany"
- given-names: "Barak"
- orcid: "https://orcid.org/0000-0003-2222-127X"
- doi: "10.5281/zenodo.4742687"
- journal: "arXiv"
+ -
+ family-names: Shoshany
+ given-names: Barak
+ orcid: https://orcid.org/0000-0003-2222-127X
+ doi: 10.5281/zenodo.4742687
+ journal: arXiv
month: 5
- title: "A C++17 Thread Pool for High-Performance Scientific Computing"
- url: "https://arxiv.org/abs/2105.00613"
+ title: A C++17 Thread Pool for High-Performance Scientific Computing
+ type: article
+ url: https://arxiv.org/abs/2105.00613
year: 2021
+repository-code: https://github.com/bshoshany/thread-pool
+title: A C++17 Thread Pool for High-Performance Scientific Computing
diff --git a/README.md b/README.md
index d6e0c8f..6b1db3e 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,16 @@
-[](https://doi.org/10.5281/zenodo.4742687)
-[](https://arxiv.org/abs/2105.00613)
+[
+](https://doi.org/10.5281/zenodo.4742687)
+[
+](https://arxiv.org/abs/2105.00613)
[](https://github.com/bshoshany/thread-pool/blob/master/LICENSE.txt)
-
-
-
-[](https://github.com/bshoshany/thread-pool)
-[](https://vscode.dev/github/bshoshany/thread-pool)
+[](https://cppreference.com)
+[](https://github.com/bshoshany/thread-pool/releases)
+[](https://vcpkg.io/)
+[](https://conan.io/center/recipes/bshoshany-thread-pool)
+[](https://github.com/bshoshany/thread-pool/stargazers)
+[](https://github.com/bshoshany/thread-pool/forks)
+[](https://vscode.dev/github/bshoshany/thread-pool)
+[](https://baraksh.com/)
# `BS::thread_pool`: a fast, lightweight, and easy-to-use C++17 thread pool library
@@ -14,43 +19,64 @@ Email: \
Website: \
GitHub:
-This is the complete documentation for v3.5.0 of the library, released on 2023-05-25.
+This is the complete documentation for v4.0.0 of the library, released on 2023-12-27.
* [Introduction](#introduction)
* [Motivation](#motivation)
* [Overview of features](#overview-of-features)
* [Compiling and compatibility](#compiling-and-compatibility)
- * [Installing using vcpkg](#installing-using-vcpkg)
- * [Installing using Conan](#installing-using-conan)
* [Getting started](#getting-started)
- * [Including the library](#including-the-library)
+ * [Installing the library](#installing-the-library)
* [Constructors](#constructors)
* [Getting and resetting the number of threads in the pool](#getting-and-resetting-the-number-of-threads-in-the-pool)
- * [Finding the version of the package](#finding-the-version-of-the-package)
-* [Submitting and waiting for tasks](#submitting-and-waiting-for-tasks)
- * [Submitting tasks to the queue with futures](#submitting-tasks-to-the-queue-with-futures)
- * [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)
- * [Waiting with a timeout](#waiting-with-a-timeout)
- * [Submitting class member functions to the queue](#submitting-class-member-functions-to-the-queue)
- * [Passing task arguments by reference](#passing-task-arguments-by-reference)
+ * [Finding the version of the library](#finding-the-version-of-the-library)
+* [Submitting tasks to the queue](#submitting-tasks-to-the-queue)
+ * [Submitting tasks with no arguments and receiving a future](#submitting-tasks-with-no-arguments-and-receiving-a-future)
+ * [Submitting tasks with arguments and receiving a future](#submitting-tasks-with-arguments-and-receiving-a-future)
+ * [Detaching and waiting for tasks](#detaching-and-waiting-for-tasks)
+ * [Waiting for submitted or detached tasks with a timeout](#waiting-for-submitted-or-detached-tasks-with-a-timeout)
+ * [Class member functions as tasks](#class-member-functions-as-tasks)
* [Parallelizing loops](#parallelizing-loops)
* [Automatic parallelization of loops](#automatic-parallelization-of-loops)
- * [Loops with return values](#loops-with-return-values)
* [Parallelizing loops without futures](#parallelizing-loops-without-futures)
-* [Helper classes](#helper-classes)
- * [Synchronizing printing to an output stream](#synchronizing-printing-to-an-output-stream)
- * [Handling multiple futures at once](#handling-multiple-futures-at-once)
- * [Measuring execution time](#measuring-execution-time)
-* [Other features](#other-features)
+ * [Parallelizing individual indices vs. blocks](#parallelizing-individual-indices-vs-blocks)
+ * [Loops with return values](#loops-with-return-values)
+ * [Parallelizing sequences](#parallelizing-sequences)
+ * [More about `BS::multi_future`](#more-about-bsmulti_futuret)
+* [Utility classes](#utility-classes)
+ * [Synchronizing printing to a stream with `BS::synced_stream`](#synchronizing-printing-to-a-stream-with-bssynced_stream)
+ * [Measuring execution time with `BS::timer`](#measuring-execution-time-with-bstimer)
+ * [Sending simple signals between threads with `BS::signaller`](#sending-simple-signals-between-threads-with-bssignaller)
+* [Managing tasks](#managing-tasks)
* [Monitoring the tasks](#monitoring-the-tasks)
- * [Pausing the workers](#pausing-the-workers)
* [Purging tasks](#purging-tasks)
* [Exception handling](#exception-handling)
-* [Testing the package](#testing-the-package)
+ * [Getting information about the threads](#getting-information-about-the-threads)
+ * [Thread pool initialization functions](#thread-pool-initialization-functions)
+ * [Passing task arguments by constant reference](#passing-task-arguments-by-constant-reference)
+* [Optional features](#optional-features)
+ * [Pausing the pool](#pausing-the-pool)
+ * [Avoiding wait deadlocks](#avoiding-wait-deadlocks)
+ * [Accessing native thread handles](#accessing-native-thread-handles)
+ * [Setting task priority](#setting-task-priority)
+* [Testing the library](#testing-the-library)
* [Automated tests](#automated-tests)
* [Performance tests](#performance-tests)
-* [The light version of the package](#the-light-version-of-the-package)
+* [Installing the library using package managers](#installing-the-library-using-package-managers)
+ * [Installing using vcpkg](#installing-using-vcpkg)
+ * [Installing using Conan](#installing-using-conan)
+ * [Installing using Meson](#installing-using-meson)
+ * [Installing using CMake with CPM](#installing-using-cmake-with-cpm)
+* [Complete library reference](#complete-library-reference)
+ * [Main thread pool header file (`BS_thread_pool.hpp`)](#main-thread-pool-header-file-bs_thread_poolhpp)
+ * [The `BS::thread_pool` class](#the-bsthread_pool-class)
+ * [Optional features for the `BS::thread_pool` class](#optional-features-for-the-bsthread_pool-class)
+ * [The `BS::this_thread` namespace](#the-bsthis_thread-namespace)
+ * [The `BS::multi_future` class](#the-bsmulti_futuret-class)
+ * [Utility header file (`BS_thread_pool_utils.hpp`)](#utility-header-file-bs_thread_pool_utilshpp)
+ * [The `BS::signaller` class](#the-bssignaller-class)
+ * [The `BS::synced_stream` class](#the-bssynced_stream-class)
+ * [The `BS::timer` class](#the-bstimer-class)
* [About the project](#about-the-project)
* [Issue and pull request policy](#issue-and-pull-request-policy)
* [Acknowledgements](#acknowledgements)
@@ -68,11 +94,11 @@ The library presented here contains a C++ thread pool class, `BS::thread_pool`,
The user submits tasks to be executed into a queue. Whenever a thread becomes available, it retrieves the next task from the queue and executes it. The pool automatically produces an `std::future` for each task, which allows the user to wait for the task to finish executing and/or obtain its eventual return value, if applicable. Threads and tasks are autonomously managed by the pool in the background, without requiring any input from the user aside from submitting the desired tasks.
-The design of this package was guided by four important principles. First, *compactness*: the entire library consists of just one small self-contained header file, with no other components or dependencies. Second, *portability*: the package only utilizes the C\+\+17 standard library, without relying on any compiler extensions or 3rd-party libraries, and is therefore compatible with any modern standards-conforming C\+\+17 compiler on any platform. Third, *ease of use*: the package is extensively documented, and programmers of any level should be able to use it right out of the box.
+The design of this library is guided by four important principles. First, *compactness*: the entire library consists of just one self-contained header file, with no other components or dependencies, aside from a small self-contained header file with optional utilities. Second, *portability*: the library only utilizes the C\+\+17 standard library, without relying on any compiler extensions or 3rd-party libraries, and is therefore compatible with any modern standards-conforming C\+\+17 compiler on any platform. Third, *ease of use*: the library is extensively documented, and programmers of any level should be able to use it right out of the box.
The fourth and final guiding principle is *performance*: each and every line of code in this library was carefully designed with maximum performance in mind, and performance was tested and verified on a variety of compilers and platforms. Indeed, the library was originally designed for use in the author's own computationally-intensive scientific computing projects, running both on high-end desktop/laptop computers and high-performance computing nodes.
-Other, more advanced multithreading libraries may offer more features and/or higher performance. However, they typically consist of a vast codebase with multiple components and dependencies, and involve complex APIs that require a substantial time investment to learn. This library is not intended to replace these more advanced libraries; instead, it was designed for users who don't require very advanced features, and prefer a simple and lightweight package that is easy to learn and use and can be readily incorporated into existing or new projects.
+Other, more advanced multithreading libraries may offer more features and/or higher performance. However, they typically consist of a vast codebase with multiple components and dependencies, and involve complex APIs that require a substantial time investment to learn. This library is not intended to replace these more advanced libraries; instead, it was designed for users who don't require very advanced features, and prefer a simple and lightweight library that is easy to learn and use and can be readily incorporated into existing or new projects.
### Overview of features
@@ -87,26 +113,32 @@ Other, more advanced multithreading libraries may offer more features and/or hig
* Header-only: no need to install or build the library.
* Self-contained: no external requirements or dependencies.
* Portable: uses only the C++ standard library, and works with any C++17-compliant compiler.
- * Only 247 lines of code, excluding comments, blank lines, and lines containing only a single brace.
- * A stand-alone "light version" of the C++ thread pool is also available in the `BS_thread_pool_light.hpp` header file, with only 115 lines of code.
+ * Only 304 lines of code, excluding comments, blank lines, and lines containing only a single brace, with all optional features disabled.
+ * Only 396 lines of code across both header files with all optional features enabled and including all optional utilities.
* **Easy to use:**
- * Very simple operation, using a handful of member functions.
- * Every task submitted to the queue using the `submit()` member function automatically generates an `std::future`, which can be used to wait for the task to finish executing and/or obtain its eventual return value.
- * Loops can be automatically parallelized into any number of parallel tasks using the `parallelize_loop()` member function, which returns a `BS::multi_future` (see below) that can be used to track the execution of all parallel tasks at once.
- * If futures are not needed, tasks may be submitted using `push_task()`, and loops can be parallelized using `push_loop()` - sacrificing convenience for even greater performance.
+ * Very simple operation, using only a handful of member functions, with additional member functions for more advanced use.
+ * Every task submitted to the queue using the `submit_task()` member function automatically generates an `std::future`, which can be used to wait for the task to finish executing and/or obtain its eventual return value.
+ * Loops can be automatically parallelized into any number of tasks using the `submit_loop()` member function, which returns a `BS::multi_future` that can be used to track the execution of all parallel tasks at once.
+ * If futures are not needed, tasks may be submitted using `detach_task()`, and loops can be parallelized using `detach_loop()` - sacrificing convenience for even greater performance. In that case, `wait()`, `wait_for()`, and `wait_until()` can be used to wait for all the tasks in the queue to complete.
* The code is thoroughly documented using Doxygen comments - not only the interface, but also the implementation, in case the user would like to make modifications.
- * The included test program `BS_thread_pool_test.cpp` can be used to perform exhaustive automated tests and benchmarks, and also serves as a comprehensive example of how to properly use the package.
-* **Helper classes:**
- * Track the execution of multiple futures at once using the `BS::multi_future` helper class.
- * Synchronize output to a stream from multiple threads in parallel using the `BS::synced_stream` helper class.
- * Easily measure execution time for benchmarking purposes using the `BS::timer` helper class.
+ * The included test program `BS_thread_pool_test.cpp` can be used to perform exhaustive automated tests and benchmarks, and also serves as a comprehensive example of how to properly use the library. The included PowerShell script `BS_thread_pool_test.ps1` provides a portable way to run the tests with multiple compilers.
+* **Utility classes:**
+ * The optional header file `BS_thread_pool_utils.hpp` contains several useful utility classes.
+ * Send simple signals between threads using the `BS::signaller` utility class.
+ * Synchronize output to a stream from multiple threads in parallel using the `BS::synced_stream` utility class.
+ * Easily measure execution time for benchmarking purposes using the `BS::timer` utility class.
* **Additional features:**
- * Easily wait for all tasks in the queue to complete using the `wait_for_tasks()`, `wait_for_tasks_duration()`, and `wait_for_tasks_until()` member functions.
+ * Assign a priority to each task using the optional task priority feature. Tasks with higher priorities will be executed first.
+ * Submit a sequence of tasks enumerated by indices to the queue using `detach_sequence()` and `submit_sequence()`.
* Change the number of threads in the pool safely and on-the-fly as needed using the `reset()` member function.
* Monitor the number of queued and/or running tasks using the `get_tasks_queued()`, `get_tasks_running()`, and `get_tasks_total()` member functions.
+ * Get the current thread count of the pool using `get_thread_count()`.
* Freely pause and resume the pool using the `pause()`, `unpause()`, and `is_paused()` member functions; when paused, threads do not retrieve new tasks out of the queue.
* Purge all tasks currently waiting in the queue with the `purge()` member function.
- * Catch exceptions thrown by tasks submitted using `submit()` or `parallelize_loop()` from the main thread through their futures.
+ * Catch exceptions thrown by tasks submitted using `submit_task()` or `submit_loop()` from the main thread through their futures.
+ * Run an initialization function in each thread before it starts to execute any submitted tasks.
+ * Get the pool index of the current thread using `BS::this_thread::get_index()` and a pointer to the pool that owns the thread using `BS::this_thread::get_pool()`.
+ * Get the unique thread IDs for all threads in the pool using `get_thread_ids()` or the implementation-defined thread handles using the optional `get_native_handles()` member function.
* Submit class member functions to the pool, either applied to a specific object or from within the object itself.
* Pass arguments to tasks by value, reference, or constant reference.
* Under continuous and active development. Bug reports and feature requests are welcome, and should be made via [GitHub issues](https://github.com/bshoshany/thread-pool/issues).
@@ -115,22 +147,22 @@ Other, more advanced multithreading libraries may offer more features and/or hig
This library should successfully compile on any C++17 standard-compliant compiler, on all operating systems and architectures for which such a compiler is available. Compatibility was verified with a 24-core (8P+16E) / 32-thread Intel i9-13900K CPU using the following compilers and platforms:
-* Windows 11 build 22621.1702:
- * [Clang](https://clang.llvm.org/) v16.0.4
- * [GCC](https://gcc.gnu.org/) v13.1.0 ([WinLibs build](https://winlibs.com/))
- * [MSVC](https://docs.microsoft.com/en-us/cpp/) v19.36.32532
-* Ubuntu 23.04:
- * [Clang](https://clang.llvm.org/) v16.0.0
- * [GCC](https://gcc.gnu.org/) v13.0.1
+* Windows 11 build 22631.2861:
+ * [Clang](https://clang.llvm.org/) v17.0.6
+ * [GCC](https://gcc.gnu.org/) v13.2.0 ([MSYS2 build](https://www.msys2.org/))
+ * [MSVC](https://docs.microsoft.com/en-us/cpp/) v19.38.33133
+* Ubuntu 23.10:
+ * [Clang](https://clang.llvm.org/) v17.0.6
+ * [GCC](https://gcc.gnu.org/) v13.2.0
-In addition, this library was tested on a [Digital Research Alliance of Canada](https://alliancecan.ca/en) node equipped with two 20-core / 40-thread Intel Xeon Gold 6148 CPUs (for a total of 40 cores and 80 threads), running CentOS Linux 7.9.2009, using [GCC](https://gcc.gnu.org/) v12.2.0.
+In addition, this library was tested on a [Digital Research Alliance of Canada](https://alliancecan.ca/en) node equipped with two 20-core / 40-thread Intel Xeon Gold 6148 CPUs (for a total of 40 cores and 80 threads), running CentOS Linux 7.9.2009, using [GCC](https://gcc.gnu.org/) v13.2.0.
-The test program `BS_thread_pool_test.cpp` was compiled without warnings (with the warning flags `-Wall -Wextra -Wconversion -Wsign-conversion -Wpedantic -Weffc++ -Wshadow` in GCC/Clang and `/W4` in MSVC), executed, and successfully completed all [automated tests](#testing-the-package) and benchmarks using all of the compilers and systems mentioned above.
+The test program `BS_thread_pool_test.cpp` was compiled without warnings (with the warning flags `-Wall -Wextra -Wconversion -Wsign-conversion -Wpedantic -Weffc++ -Wshadow` in GCC/Clang and `/W4` in MSVC), executed, and successfully completed all [automated tests](#testing-the-library) and benchmarks using all of the compilers and systems mentioned above.
As this library requires C\+\+17 features, the code must be compiled with C\+\+17 support:
* For Clang or GCC, use the `-std=c++17` flag. On Linux, you will also need to use the `-pthread` flag to enable the POSIX threads library.
-* For MSVC, use `/std:c++17`, and preferably also `/permissive-` to ensure standards conformance.
+* For MSVC, use `/std:c++17`, and also `/permissive-` to ensure standards conformance.
For maximum performance, it is recommended to compile with all available compiler optimizations:
@@ -142,43 +174,23 @@ As an example, to compile the test program `BS_thread_pool_test.cpp` with warnin
* On Linux with GCC: `g++ BS_thread_pool_test.cpp -std=c++17 -O3 -Wall -Wextra -Wconversion -Wsign-conversion -Wpedantic -Weffc++ -Wshadow -pthread -o BS_thread_pool_test`
* On Linux with Clang: replace `g++` with `clang++`.
* On Windows with GCC or Clang: replace `-o BS_thread_pool_test` with `-o BS_thread_pool_test.exe` and remove `-pthread`.
-* On Windows with MSVC: `cl BS_thread_pool_test.cpp /std:c++17 /permissive- /O2 /W4 /EHsc /Fe:BS_thread_pool_test.exe`
-
-### Installing using vcpkg
-
-If you are using the [vcpkg](https://github.com/microsoft/vcpkg) C/C++ library manager, you can easily download and install this package with the following commands.
-
-On Linux/macOS:
-
-```none
-./vcpkg install bshoshany-thread-pool
-```
-
-On Windows:
-
-```none
-.\vcpkg install bshoshany-thread-pool:x86-windows bshoshany-thread-pool:x64-windows
-```
-
-The C++ thread pool will then be available automatically in the build system you integrated vcpkg with (e.g. MSBuild or CMake). Simply write `#include "BS_thread_pool.hpp"` in any project to use the thread pool, without having to copy to file into the project first. I will update the vcpkg port with each new release, so it will be updated automatically when you run `vcpkg upgrade`.
-
-Please see the [vcpkg repository](https://github.com/microsoft/vcpkg) for more information on how to use vcpkg.
-
-### Installing using Conan
-
-If you are using the [Conan](https://conan.io/) C/C++ package manager, please refer to [this package's page on ConanCenter](https://conan.io/center/bshoshany-thread-pool) to learn how to use Conan to include this package in your project with various build systems.
+* On Windows with MSVC: `cl BS_thread_pool_test.cpp /std:c++17 /permissive- /O2 /W4 /EHsc /Fo:BS_thread_pool_test.obj /Fe:BS_thread_pool_test.exe`
## Getting started
-### Including the library
+### Installing the library
-If you are not using a C++ library manager (such as vcpkg), simply download the [latest release](https://github.com/bshoshany/thread-pool/releases) from the GitHub repository, place the single header file `BS_thread_pool.hpp` from the `include` folder of the repository in the desired folder, and include it in your program:
+To install `BS::thread_pool`, simply download the [latest release](https://github.com/bshoshany/thread-pool/releases) from the GitHub repository, place the header file `BS_thread_pool.hpp` from the `include` folder in the desired folder, and include it in your program:
```cpp
#include "BS_thread_pool.hpp"
```
-The thread pool will now be accessible via the `BS::thread_pool` class.
+The thread pool will now be accessible via the `BS::thread_pool` class. For an even quicker installation, you can download the header file itself directly [at this URL](https://raw.githubusercontent.com/bshoshany/thread-pool/master/include/BS_thread_pool.hpp).
+
+This library also comes with an independent utilities header file `BS_thread_pool_utils.hpp`, which is not required to use the thread pool, but provides some utility classes that may be helpful for multithreading. This header file also resides in the `include` folder. It can be downloaded directly [at this URL](https://raw.githubusercontent.com/bshoshany/thread-pool/master/include/BS_thread_pool_utils.hpp).
+
+This library is also available on various package managers and build system, including [vcpkg](https://vcpkg.io/), [Conan](https://conan.io/), [Meson](https://mesonbuild.com/), and [CMake](https://cmake.org/) with [CPM](https://github.com/cpm-cmake/CPM.cmake). Please [see below](#installing-the-library-using-package-managers) for more details.
### Constructors
@@ -196,9 +208,7 @@ Optionally, a number of threads different from the hardware concurrency can be s
BS::thread_pool pool(12);
```
-If your program's main thread only submits tasks to the thread pool and waits for them to finish, and does not perform any computationally intensive tasks on its own, then it is recommended to use the default value for the number of threads. This ensures that all of the threads available in the hardware will be put to work while the main thread waits.
-
-However, if your main thread does perform computationally intensive tasks on its own, then it is recommended to use the value `std::thread::hardware_concurrency() - 1` for the number of threads. In this case, the main thread plus the thread pool will together take up exactly all the threads available in the hardware.
+Usually, when the thread pool is used, a program's main thread should only submit tasks to the thread pool and wait for them to finish, and should not perform any computationally intensive tasks on its own. In that case, it is recommended to use the default value for the number of threads. This ensures that all of the threads available in the hardware will be put to work while the main thread waits.
### Getting and resetting the number of threads in the pool
@@ -208,9 +218,9 @@ It is generally unnecessary to change the number of threads in the pool after it
`reset()` will wait for all currently running tasks to be completed, but will leave the rest of the tasks in the queue. Then it will destroy the thread pool and create a new one with the desired new number of threads, as specified in the function's argument (or the hardware concurrency if no argument is given). The new thread pool will then resume executing the tasks that remained in the queue and any new submitted tasks.
-### Finding the version of the package
+### Finding the version of the library
-If desired, the version of this package may be read during compilation time from the macro `BS_THREAD_POOL_VERSION`. The value will be a string containing the version number and release date. For example:
+If desired, the version of this library may be read during compilation time from the macro `BS_THREAD_POOL_VERSION`. The value will be a string containing the version number and release date. For example:
```cpp
std::cout << "Thread pool library version is " << BS_THREAD_POOL_VERSION << ".\n";
@@ -219,60 +229,31 @@ std::cout << "Thread pool library version is " << BS_THREAD_POOL_VERSION << ".\n
Sample output:
```none
-Thread pool library version is v3.5.0 (2023-05-25).
+Thread pool library version is v4.0.0 (2023-12-27).
```
This can be used, for example, to allow the same code to work with several incompatible versions of the library.
-## Submitting and waiting for tasks
+## Submitting tasks to the queue
-### Submitting tasks to the queue with futures
+### Submitting tasks with no arguments and receiving a future
-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).
+In this section we will learn how to submit a task with no arguments, but potentially with a return value, to the queue. Once a task has been submitted, 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), unless task priority is enabled (see below).
-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:
+For example, if the pool has 8 threads and an empty queue, and we submitted 16 tasks, then we should expect the first 8 tasks to be executed in parallel, with the remaining tasks being picked up by the threads one by one as each thread finishes executing its first task, until no tasks are left in the queue.
-```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 member function `submit_task()` is used to submit tasks to the queue. It takes exactly one input, the task to submit. This task must be a function with no arguments, but it can have a return value. The return value is an `std::future` associated to the task.
If the submitted function has a return value of type `T`, then the future will be of type `std::future`, and will be set to the return value when the function finishes its execution. If the submitted function does not have a return value, then the future will be an `std::future`, which will not return any value but may still be used to wait for the function to finish.
-Using `auto` for the return value of `submit()` means the compiler will automatically detect which instance of the template `std::future` to use. However, specifying the particular type `std::future`, as in the examples below, is recommended for increased readability.
+Using `auto` for the return value of `submit_task()` means the compiler will automatically detect which instance of the template `std::future` to use. However, specifying the particular type `std::future`, as in the examples below, is recommended for increased readability.
-To wait until the task finishes, use the member function `wait()` of the future. To obtain the return value, use the member function `get()`, which will also automatically wait for the task to finish if it hasn't yet. For example:
+To wait until the task finishes, use the member function `wait()` of the future. To obtain the return value, use the member function `get()`, which will also automatically wait for the task to finish if it hasn't yet. Here is a simple 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, waiting for it to finish running if needed.
-auto my_return_value = my_future.get();
-```
-
-Here are some more concrete examples. The following program will print out `42`:
-
-```cpp
-#include "BS_thread_pool.hpp"
-
-int main()
-{
- BS::thread_pool pool;
- std::future my_future = pool.submit([] { return 42; });
- std::cout << my_future.get();
-}
-```
-
-Here we used a [lambda expression](https://en.cppreference.com/w/cpp/language/lambda) to quickly define the function on-the-fly. However, we can also use a previously-defined function:
-
-```cpp
-#include "BS_thread_pool.hpp"
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include // std::future
+#include // std::cout
int the_answer()
{
@@ -282,161 +263,283 @@ int the_answer()
int main()
{
BS::thread_pool pool;
- std::future my_future = pool.submit(the_answer);
- std::cout << my_future.get();
+ std::future my_future = pool.submit_task(the_answer);
+ std::cout << my_future.get() << '\n';
}
```
-The following is an example of submitting a function with arguments:
+In this example we submitted the function `the_answer()`, which returns an `int`. The member function `submit_task()` of the pool therefore returned an `std::future`. We then used used the `get()` member function of the future to get the return value, and printed it out.
+
+In addition to submitted a pre-defined function, we can also use a [lambda expression](https://en.cppreference.com/w/cpp/language/lambda) to quickly define the task on-the-fly. Rewriting the previous example in terms of a lambda expression, we get:
```cpp
-#include "BS_thread_pool.hpp"
-
-int multiply(const int a, const int b)
-{
- return a * b;
-}
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include // std::future
+#include // std::cout
int main()
{
BS::thread_pool pool;
- std::future my_future = pool.submit(multiply, 6, 7);
- std::cout << my_future.get();
+ std::future my_future = pool.submit_task([]{ return 42; });
+ std::cout << my_future.get() << '\n';
}
```
-Finally, here is an example of submitting a function with no return value and then using the future to wait for it to finish executing:
+Here, the lambda expression `[]{ return 42; }` has two parts:
+
+1. An empty capture clause, denoted by `[]`. This signifies to the compiler that a lambda expression is being defined.
+2. A code block `{ return 42; }` that simply returns the value `42`.
+
+It is generally simpler and faster to submit lambda expressions rather than pre-defined functions, especially due to the ability to capture local variables, which we will discuss in the next section.
+
+Of course, tasks do not have to return values. In the following example, we submit a function with no return value and then using the future to wait for it to finish executing:
```cpp
-#include "BS_thread_pool.hpp"
-
-void sleep()
-{
- std::this_thread::sleep_for(std::chrono::milliseconds(1000));
-}
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include // std::chrono
+#include // std::future
+#include // std::cout
+#include // std::this_thread
int main()
{
BS::thread_pool pool;
- std::future my_future = pool.submit(sleep);
- std::cout << "Waiting... ";
+ const std::future my_future = pool.submit_task(
+ []
+ {
+ std::this_thread::sleep_for(std::chrono::milliseconds(500));
+ });
+ std::cout << "Waiting for the task to complete... ";
my_future.wait();
- std::cout << "Done.";
+ std::cout << "Done." << '\n';
}
```
-Here, the command `std::this_thread::sleep_for(std::chrono::milliseconds(1000))` instructs the thread to sleep for 1 second.
+Here we split the lambda into multiple lines to make it more readable. The command `std::this_thread::sleep_for(std::chrono::milliseconds(500))` instructs the task to simply sleep for 500 milliseconds, simulating a computationally-intensive task.
-### Submitting tasks to the queue without futures
+### Submitting tasks with arguments and receiving a future
-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 the main thread or with other tasks without using futures, such as via condition variables. In such cases, you may wish to avoid the overhead involved in assigning a future to the task in order to increase performance.
+As stated in the previous section, tasks submitted using `submit_task()` cannot have any arguments. However, it is easy to submit tasks with argument either by wrapping the function in a lambda or using lambda captures directly. Here are two examples.
-The member function `push_task()` allows you to submit a task to the queue without generating a future for it. The task can have any number of arguments, but it cannot have a return value. For example:
+The following is an example of submitting a pre-defined function with arguments by wrapping it with a lambda:
```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);
-```
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include // std::future
+#include // std::cout
-**Warning:** Since `push_task()` does not return a future, there is no built-in way for the user to know when the task finishes executing. You must use either `wait_for_tasks()` (see below), or some other method such as condition variables, to ensure that the task finishes executing before trying to use anything that depends on its output. Otherwise, bad things will happen!
-
-### 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 can use the member function `wait_for_tasks()`.
-
-Consider, for example, the following code:
-
-```cpp
-#include "BS_thread_pool.hpp"
+double multiply(const double lhs, const double rhs)
+{
+ return lhs * rhs;
+}
int main()
{
- BS::thread_pool pool(5);
- int squares[100];
- for (int i = 0; i < 100; ++i)
- pool.push_task(
- [&squares, i]
- {
- std::this_thread::sleep_for(std::chrono::milliseconds(50));
- squares[i] = i * i;
- });
- std::cout << squares[50];
+ BS::thread_pool pool;
+ std::future my_future = pool.submit_task(
+ []
+ {
+ return multiply(6, 7);
+ });
+ std::cout << my_future.get() << '\n';
}
```
-The output will most likely be garbage, since the task that modifies `squares[50]` has not yet finished executing by the time we try to access that element - it's still waiting in the queue. One solution would be to use `submit()` instead of `push_task()`, but perhaps we don't want the overhead of generating 100 different futures. Instead, simply adding the line
+As you can see, to pass the arguments to `multiply` we simply called `multiply(6, 7)` explicitly inside a lambda. If the arguments are not literals, we need to use the lambda capture clause to capture the arguments from the local scope:
```cpp
-pool.wait_for_tasks();
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include // std::future
+#include // std::cout
+
+double multiply(const double lhs, const double rhs)
+{
+ return lhs * rhs;
+}
+
+int main()
+{
+ BS::thread_pool pool;
+ constexpr double first = 6;
+ constexpr double second = 7;
+ std::future my_future = pool.submit_task(
+ [first, second]
+ {
+ return multiply(first, second);
+ });
+ std::cout << my_future.get() << '\n';
+}
```
-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 `squares`, and the code will print out the value `2500` as expected.
+We could even get rid of the `multiply` function entirely and put everything inside a lambda, if desired:
-Note, however, that `wait_for_tasks()` will wait for **all** the tasks in the queue, including those that are unrelated to the `for` loop. Using [`parallelize_loop()`](#parallelizing-loops) would make much more sense in this particular case, as it will allow waiting only for the tasks related to the loop.
+```cpp
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include // std::future
+#include // std::cout
-**Warning:** Never call `wait_for_tasks()` from within a thread of the same thread pool, for example `pool.push_task([] { pool.wait_for_tasks(); })`, as that will cause it to wait forever!
+int main()
+{
+ BS::thread_pool pool;
+ constexpr double first = 6;
+ constexpr double second = 7;
+ std::future my_future = pool.submit_task(
+ [first, second]
+ {
+ return first * second;
+ });
+ std::cout << my_future.get() << '\n';
+}
+```
-### Waiting with a timeout
+### Detaching and waiting for tasks
-Sometimes you may wish to wait for the tasks to complete, but only for a certain amount of time, or until a specific point in time. For example, if the tasks have not yet completed after some time, you may wish to let the user know that there is a delay. This can be achieved using two member functions:
+Usually, it is best to submit a task to the queue using `submit_task()`. 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 the main thread or with other tasks without using futures, such as via condition variables.
-* `wait_for_tasks_duration()` waits for the tasks to be completed, but stops waiting after the specified duration, given as an argument of type `std::chrono::duration`, has passed.
-* `wait_for_tasks_until()` waits for the tasks to be completed, but stops waiting after the specified time point, given as an argument of type `std::chrono::time_point`, has been reached.
+In such cases, you may wish to avoid the overhead involved in assigning a future to the task in order to increase performance. This is called "detaching" the task, as the task detaches from the main thread and runs independently.
+
+Detaching tasks is done using the `detach_task()` member function, which allows you to detach a task to the queue without generating a future for it. The task can have any number of arguments, but it cannot have a return value, as there would be no way for the main thread to retrieve that value.
+
+Since `detach_task()` does not return a future, there is no built-in way for the user to know when the task finishes executing. You must manually ensure that the task finishes executing before trying to use anything that depends on its output. Otherwise, bad things will happen!
+
+`BS::thread_pool` provides the member function `wait()` to facilitate waiting for all of the tasks in the queue to complete, whether they were detached or submitted with a future. The `wait()` member function works similarly to the `wait()` member function of `std::future`. Consider, for example, the following code:
+
+```cpp
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include // std::chrono
+#include // std::cout
+#include // std::this_thread
+
+int main()
+{
+ BS::thread_pool pool;
+ int result = 0;
+ pool.detach_task(
+ [&result]
+ {
+ std::this_thread::sleep_for(std::chrono::milliseconds(100));
+ result = 42;
+ });
+ std::cout << result << '\n';
+}
+```
+
+This program first defines a local variable named `result` and initializes it to `0`. It then detaches a task in the form of a lambda expression. Note that the lambda captures `result` **by reference**, as indicated by the `&` in front of it. This means that the task can modify `result`, and any such modification will be reflected in the main thread. The task changes `result` to `42`, but it first sleeps for 100 milliseconds. When the main thread prints out the value of `result`, the task has not yet had time to modify its value, since it is still sleeping. Therefore, the program will print out the initial value `0`.
+
+To wait for the task to complete, we must use the `wait()` member function after detaching it:
+
+```cpp
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include // std::chrono
+#include // std::cout
+#include // std::this_thread
+
+int main()
+{
+ BS::thread_pool pool;
+ int result = 0;
+ pool.detach_task(
+ [&result]
+ {
+ std::this_thread::sleep_for(std::chrono::milliseconds(100));
+ result = 42;
+ });
+ pool.wait();
+ std::cout << result << '\n';
+}
+```
+
+Now the program will print out the value `42`, as expected. Note, however, that `wait()` will wait for **all** the tasks in the queue, including any other tasks that were potentially submitted before or after the one we care about. If we want to wait for just one task, `submit_task()` would be a better choice.
+
+### Waiting for submitted or detached tasks with a timeout
+
+Sometimes you may wish to wait for the tasks to complete, but only for a certain amount of time, or until a specific point in time. For example, if the tasks have not yet completed after some time, you may wish to let the user know that there is a delay.
+
+For task submitted with futures using `submit_task()`, this can be achieved using two member functions of `std::future`:
+
+* `wait_for()` waits for the task to be completed, but stops waiting after the specified duration, given as an argument of type `std::chrono::duration`, has passed.
+* `wait_until()` waits for the task to be completed, but stops waiting after the specified time point, given as an argument of type `std::chrono::time_point`, has been reached.
+
+In both cases, the functions will return `future_status::ready` if the future is ready, meaning the task is finished and its return value, if any, has been obtained. However, it will return `std::future_status::timeout` if the future is not yet ready by the time the timeout has expired.
Here is an example:
```cpp
-#include "BS_thread_pool.hpp"
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include // std::chrono
+#include // std::future
+#include // std::cout
+#include // std::this_thread
int main()
{
BS::thread_pool pool;
- std::atomic done = false;
- pool.push_task(
- [&done]
+ const std::future my_future = pool.submit_task(
+ []
{
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
- done = true;
+ std::cout << "Task done!\n";
});
while (true)
{
- pool.wait_for_tasks_duration(std::chrono::milliseconds(200));
- if (!done)
- std::cout << "Sorry, task is not done yet.\n";
+ if (my_future.wait_for(std::chrono::milliseconds(200)) != std::future_status::ready)
+ std::cout << "Sorry, the task is not done yet.\n";
else
break;
}
- std::cout << "Task done!\n";
}
```
-The output is:
+The output should look similar to this:
```none
-Sorry, task is not done yet.
-Sorry, task is not done yet.
-Sorry, task is not done yet.
-Sorry, task is not done yet.
+Sorry, the task is not done yet.
+Sorry, the task is not done yet.
+Sorry, the task is not done yet.
+Sorry, the task is not done yet.
Task done!
```
-### Submitting class member functions to the queue
+For detached tasks, since we do not have a future for them, we cannot use this method. However, `BS::thread_pool` has two member functions, also named `wait_for()` and `wait_until()`, which similarly wait for a specified duration or until a specified time point, but do so for **all** tasks (whether submitted or detached). Instead of an `std::future_status`, the thread pool's wait functions returns `true` if all tasks finished running, or `false` if the duration expired or the time point was reached but some tasks are still running.
-Consider the following program:
+Here is the same example as above, using `detach_task()` and `pool.wait_for()`:
```cpp
-#include "BS_thread_pool.hpp"
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include // std::chrono
+#include // std::cout
+#include // std::this_thread
-BS::thread_pool pool;
+int main()
+{
+ BS::thread_pool pool;
+ pool.detach_task(
+ []
+ {
+ std::this_thread::sleep_for(std::chrono::milliseconds(1000));
+ std::cout << "Task done!\n";
+ });
+ while (true)
+ {
+ if (!pool.wait_for(std::chrono::milliseconds(200)))
+ std::cout << "Sorry, the task is not done yet.\n";
+ else
+ break;
+ }
+}
+```
+
+### Class member functions as tasks
+
+Let us consider the following program:
+
+```cpp
+#include // std::cout, std::boolalpha
class flag_class
{
public:
- bool get_flag() const
+ [[nodiscard]] bool get_flag() const
{
return flag;
}
@@ -458,44 +561,61 @@ int main()
}
```
-This program creates a new object `flag_object` of the class `flag_class`, sets the flag to `true` using the member function `set_flag()`, and then prints out the flag's value. But what if you want to submit the member function `set_flag()` as a task to the thread pool?
+This program creates a new object `flag_object` of the class `flag_class`, sets the flag to `true` using the setter member function `set_flag()`, and then prints out the flag's value using the getter member function `get_flag()`.
-To submit member functions to the pool, use the following general syntax:
+What if we want to submit the member function `set_flag()` as a task to the thread pool? We simply wrap the entire statement `flag_object.set_flag(true);` from line in a lambda, and pass `flag_object` to the lambda by reference, as in this example:
```cpp
-pool.push_task(&class::function, &object, args);
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include // std::cout, std::boolalpha
+
+class flag_class
+{
+public:
+ [[nodiscard]] bool get_flag() const
+ {
+ return flag;
+ }
+
+ void set_flag(const bool arg)
+ {
+ flag = arg;
+ }
+
+private:
+ bool flag = false;
+};
+
+int main()
+{
+ BS::thread_pool pool;
+ flag_class flag_object;
+ pool.submit_task(
+ [&flag_object]
+ {
+ flag_object.set_flag(true);
+ })
+ .wait();
+ std::cout << std::boolalpha << flag_object.get_flag() << '\n';
+}
```
-The same syntax also works with `submit()`. Note that, in the second argument, you must specify the object on which the member function will be executed (unless it's a static member function, in which case you just submit it like any other function). Also note that both the first and second arguments must be **pointers**, so they must be preceded by the `&` operator.
+Of course, this will also work with `detach_task()`, if we call `wait()` on the pool itself instead of on the returned future.
-If you remove the `&` from the first argument, the code won't work with most compilers, and if you remove the `&` from the second argument, the function will act on a **copy** of the object, rather than on the object itself, so any changes made to the object will not be saved. Therefore, it's important to ensure that both arguments are pointers.
+Note that in this example, instead of getting a future from `submit_task()` and then waiting for that future, we simply called `wait()` on that future straight away. This is a common way of waiting for a task to complete if we have nothing else to do in the meantime. Note also that we passed `flag_object` by reference to the lambda, since we want to set the flag on that same object, not a copy of it (passing by value wouldn't have worked anyway, since variables captured by value are implicitly `const`).
-To make the above program submit the member function `set_flag()` to the thread pool, simply replace the line:
+Another thing you might want to do is call a member function from within the object itself, that is, from another member function. This follows a similar syntax, except that you must also capture `this` (i.e. a pointer to the current object) in the lambda. Here is an example:
```cpp
-flag_object.set_flag(true);
-```
-
-with:
-
-```cpp
-pool.push_task(&flag_class::set_flag, &flag_object, true);
-pool.wait_for_tasks();
-```
-
-Here the class is `flag_class`, the name of the function is `set_flag`, the object we want the function to act on is `flag_object`, and the argument to pass to the function is `true`.
-
-Another thing you might want to do is call a member function from within the object itself, that is, from another member function. This follows a similar syntax, except that you don't need to specify the class, and you use `this` to get a pointer to the current object (no `&` necessary, since `this` is already a pointer). Here is an example, this time using `submit()`:
-
-```cpp
-#include "BS_thread_pool.hpp"
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include // std::cout, std::boolalpha
BS::thread_pool pool;
class flag_class
{
public:
- bool get_flag() const
+ [[nodiscard]] bool get_flag() const
{
return flag;
}
@@ -507,7 +627,12 @@ public:
void set_flag_to_true()
{
- pool.submit(&flag_class::set_flag, this, true).wait();
+ pool.submit_task(
+ [this]
+ {
+ set_flag(true);
+ })
+ .wait();
}
private:
@@ -522,229 +647,342 @@ int main()
}
```
-### Passing task arguments by reference
-
-In C++, it is often crucial to pass function arguments by reference or constant reference, instead of by value. This allows the function to access the object being passed directly, rather than creating a new copy of the object.
-
-When submitting a task using `push_task()` or `submit()`, the task's arguments are always passed by value by default. To pass arguments to the task by reference, you must wrap them with `std::ref()`. Similarly, to pass arguments by constant reference, you must wrap them with `std::cref()`. Here is an example:
-
-```cpp
-#include "BS_thread_pool.hpp"
-
-BS::thread_pool pool;
-
-void increment(int& x)
-{
- ++x;
-}
-
-void print(const int& x)
-{
- std::cout << x;
-}
-
-int main()
-{
- int n = 0;
- pool.submit(increment, std::ref(n)).wait();
- pool.submit(print, std::cref(n)).wait();
-}
-```
-
-The `increment()` function takes a **reference** to an integer, and increments that integer. Passing the argument by reference guarantees that `n` itself, in the scope of `main()`, will be incremented - rather than a copy of it in the scope of `increment()`. To pass `n` by reference, we wrapped it inside `std::ref()`. Note that the program will not compile otherwise, since `increment()` only accepts arguments by reference.
-
-Similarly, the `print()` function takes a **constant reference** to an integer, and prints that integer. Passing the argument by constant reference guarantees that the variable will not be accidentally modified by the function, even though we are accessing `n` itself, rather than a copy. To pass `n` by constant reference, we wrapped it inside `std::cref()`.
+Note that in this example we defined the thread pool as a global object, so that it is accessible outside the `main()` function.
## Parallelizing loops
### Automatic parallelization of loops
-One of the most common and effective methods of parallelization is splitting a loop into smaller loops and running them in parallel. It is most effective in "embarrassingly parallel" computations, such as vector or matrix operations, where each iteration of the loop is completely independent of every other iteration. For example, if we are summing up two vectors of 1000 elements each, and we have 10 threads, we could split the summation into 10 blocks of 100 elements each, and run all the blocks in parallel, potentially increasing performance by up to a factor of 10.
+One of the most common and effective methods of parallelization is splitting a loop into smaller loops and running them in parallel. It is most effective in "embarrassingly parallel" computations, such as vector or matrix operations, where each iteration of the loop is completely independent of every other iteration.
+
+For example, if we are summing up two vectors of 1000 elements each, and we have 10 threads, we could split the summation into 10 blocks of 100 elements each, and run all the blocks in parallel, potentially increasing performance by up to a factor of 10.
`BS::thread_pool` can automatically parallelize loops. To see how this works, consider the following generic loop:
```cpp
for (T i = start; i < end; ++i)
- do_something(i);
+ loop(i);
```
where:
* `T` is any signed or unsigned integer type.
* The loop is over the range `[start, end)`, i.e. inclusive of `start` but exclusive of `end`.
-* `do_something()` is an operation performed for each loop index `i`, such as modifying an array with `end - start` elements.
+* `loop()` is an operation performed for each loop index `i`, such as modifying an array with `end - start` elements.
-This loop may be automatically parallelized and submitted to the thread pool's queue using the member function `parallelize_loop()`, which has the follows syntax:
+This loop may be automatically parallelized and submitted to the thread pool's queue using the member function `submit_loop()`, which has the follows syntax:
```cpp
-pool.parallelize_loop(start, end, loop, num_blocks);
+pool.submit_loop(start, end, loop, num_blocks);
```
where:
* `start` is the first index in the range.
- * This argument can be omitted, in which case it is assumed that the loop starts at 0. That is, `parallelize_loop(end, loop, num_blocks)` is equivalent to `parallelize_loop(0, end, loop, num_blocks)`.
* `end` is the index after the last index in the range, such that the full range is `[start, end)`. In other words, the loop will be equivalent to the one above if `start` and `end` are the same.
- * `start` and `end` should both be integers, but they need not be of the same integer type. `parallelize_loop()` will automatically determine the best type to use for the loop indices.
-* `loop()` is any function that takes two indices, `a`, and `b`, and executes only the portion of the loop in the range `[a, b)`. Typically, `loop()` will include a `for` loop of the form `for (T i = a; i < b; ++i)`.
-* `num_blocks` is the number of blocks of the form `[a, b)` to split the loop into. For example, if the range is `[0, 9)` and there are 3 blocks, then the blocks will be the ranges `[0, 3)`, `[3, 6)`, and `[6, 9)`. If possible, the blocks will be equal in size; otherwise, the last block may be a bit longer.
+ * `start` and `end` must both be of the same integer type `T`. See below for examples of what to do when they are not of the same type.
+ * Note that if `end <= start`, nothing will happen.
+* `loop()` is the function that should run in every iteration of the loop, and takes one argument, the loop index.
+* `num_blocks` is the number of blocks of the form `[a, b)` to split the loop into. For example, if the range is `[0, 9)` and there are 3 blocks, then the blocks will be the ranges `[0, 3)`, `[3, 6)`, and `[6, 9)`.
+ * The internal algorithm ensures that each of the blocks has one of two sizes, differing by 1, with the larger blocks always first, so that the tasks are as evenly distributed as possible. For example, if the range `[0, 100)` is split into 15 blocks, the result will be 10 blocks of size 7, which will be executed first, and 5 blocks of size 6.
* This argument can be omitted, in which case the number of blocks will be the number of threads in the pool.
Each block will be submitted to the thread pool's queue as a separate task. Therefore, a loop that is split into 3 blocks will be split into 3 individual tasks, which may run in parallel. If there is only one block, then the entire loop will run as one task, and no parallelization will take place.
-To parallelize the generic loop above, we use the following code:
+To parallelize the generic loop above, we use the following commands:
```cpp
-auto loop = [](const T a, const T b)
-{
- for (T i = a; i < b; ++i)
- do_something(i);
-};
-BS::multi_future loop_future = pool.parallelize_loop(start, end, loop, num_blocks);
+BS::multi_future loop_future = pool.submit_loop(start, end, loop, num_blocks);
loop_future.wait();
```
-Here we defined `loop()` as a lambda function. Of course, `loop()` could also be defined as a lambda within the call to `parallelize_loop()` itself, as in the examples below; or it could be any ordinary function, but a lambda is preferred since one typically would like to capture some of the surrounding variables.
+`submit_loop()` returns an object of the helper class template [`BS::multi_future`](#more-about-bsmulti_futuret). This is essentially a specialization of `std::vector>` with additional member functions. Each of the `num_blocks` blocks will have an `std::future` assigned to it, and all these futures will be stored inside the returned `BS::multi_future`. When `loop_future.wait()` is called, the main thread will wait until all tasks generated by `submit_loop()` finish executing, and only those tasks - not any other tasks that also happen to be in the queue. This is essentially the role of the `BS::multi_future` class: to wait for a specific **group of tasks**, in this case the tasks running the loop blocks.
-`parallelize_loop()` returns an object of the helper class template `BS::multi_future`. Each of the `num_blocks` blocks will have an `std::future` assigned to it, and all these futures will be stored inside the returned `BS::multi_future` object. When `loop_future.wait()` is called, the main thread will wait until all tasks generated by `parallelize_loop()` finish executing, and only those tasks - not any other tasks that also happen to be in the queue. This is essentially the role of the `BS::multi_future` class: to wait for a specific **group of tasks**, in this case the tasks running the loop blocks.
+What value should you use for `num_blocks`? Omitting this argument, so that the number of blocks will be equal to the number of threads in the pool, is typically a good choice. For best performance, it is recommended to do your own benchmarks to find the optimal number of blocks for each loop (you can use the [`BS::timer`](#measuring-execution-time-with-bstimer) utility class). Using fewer 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, but parallelization with too many tasks will suffer from diminishing returns.
-What value should you use for `num_blocks`? Omitting this argument, so that the number of blocks will be equal to the number of threads in the pool, is typically a good choice. For best performance, it is recommended to do your own benchmarks to find the optimal number of blocks for each loop (you can use the `BS::timer` helper class - see [below](#measuring-execution-time)). 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.
-
-As a simple example, the following code calculates and prints the squares of all integers from 0 to 99:
+As a simple example, the following code calculates and prints a table of squares of all integers from 0 to 99:
```cpp
-#include "BS_thread_pool.hpp"
+#include // std::setw
+#include // std::cout
int main()
{
- int squares[100];
- for (int i = 0; i < 100; ++i)
- {
+ constexpr unsigned int max = 100;
+ unsigned int squares[max];
+ for (unsigned int i = 0; i < max; ++i)
squares[i] = i * i;
- std::cout << i << "^2 = " << squares[i] << " ";
- }
+ for (unsigned int i = 0; i < max; ++i)
+ std::cout << std::setw(2) << i << "^2 = " << std::setw(4) << squares[i] << ((i % 5 != 4) ? " | " : "\n");
}
```
We can parallelize it as follows:
```cpp
-#include "BS_thread_pool.hpp"
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include // std::setw
+#include // std::cout
int main()
{
BS::thread_pool pool(10);
- int squares[100];
- pool.parallelize_loop(100,
- [&squares](const int a, const int b)
- {
- for (int i = a; i < b; ++i)
- squares[i] = i * i;
- })
- .wait();
- for (int i = 0; i < 100; ++i)
- std::cout << i << "^2 = " << squares[i] << " ";
+ constexpr unsigned int max = 100;
+ unsigned int squares[max];
+ const BS::multi_future loop_future = pool.submit_loop(0, max,
+ [&squares](const unsigned int i)
+ {
+ squares[i] = i * i;
+ });
+ loop_future.wait();
+ for (unsigned int i = 0; i < max; ++i)
+ std::cout << std::setw(2) << i << "^2 = " << std::setw(4) << squares[i] << ((i % 5 != 4) ? " | " : "\n");
}
```
-Since there are 10 threads, and we omitted the `num_blocks` argument, the loop will be divided into 10 blocks, each calculating 10 squares. Also, since the loop starts from 0, we did not need to specify the first index.
+Since there are 10 threads, and we omitted the `num_blocks` argument, the loop will be divided into 10 blocks, each calculating 10 squares.
-In this example, instead of storing the `BS::multi_future` object and then using it to wait, we simply called the `wait()` member function directly on the temporary object returned by `parallelize_loop()` without storing it anywhere. This is a convenient shortcut when we have nothing else to do while waiting.
+Note that `submit_loop()` was executed with the explicit template parameter ``. The reason is that the two loop indices must be of the same type. However, here `max` is a `unsigned int`, while `0` is a (signed) `int`, so the types do not match, and the code will not compile unless we force the `0` to be of the right type. This can be done most elegantly by specifying the type of the indices explicitly using the template parameter.
-Notice that here we parallelized the calculation of the squares, but we did not parallelize printing the results. This is for two reasons:
+The reason this is not done automatically (e.g. using [`std::common_type`](https://en.cppreference.com/w/cpp/types/common_type) is that it may result in accidentally casting negative indices to an unsigned type, or integer indices to a too narrow integer type, which may lead to an incorrect loop range.
+
+We could also cast the `0` explicitly to unsigned int, but that doesn't look as nice:
+
+```cpp
+pool.submit_loop(static_cast(0), max, /* ... */);
+```
+
+Or we could use a C-style cast:
+
+```cpp
+pool.submit_loop((unsigned int)(0), max, /* ... */);
+```
+
+Or we could use an integer literal suffix:
+
+```cpp
+pool.submit_loop(0U, max, ...);
+```
+
+As a side note, notice that here we parallelized the calculation of the squares, but we did not parallelize printing the results. This is for two reasons:
1. We want to print out the squares in ascending order, and we have no guarantee that the blocks will be executed in the correct order. This is very important; you must never expect that the parallelized loop will execute at the same order as the non-parallelized loop.
-2. If we did print out the squares from within the parallel tasks, we would get a huge mess, since all 10 blocks would print to the standard output at once. [Later](#synchronizing-printing-to-an-output-stream) we will see how to synchronize printing to a stream from multiple tasks at the same time.
+2. If we did print out the squares from within the parallel tasks, we would get a huge mess, since all 10 blocks would print to the standard output at once. [Later](#synchronizing-printing-to-a-stream-with-bssynced_stream) we will see how to synchronize printing to a stream from multiple tasks at the same time.
+
+### Parallelizing loops without futures
+
+Just as in the case of [`detach_task()`](#detaching-and-waiting-for-tasks) vs. [`submit_task()`](#submitting-tasks-with-no-arguments-and-receiving-a-future), sometimes you may want to parallelize a loop, but you don't need it to return a `BS::multi_future`. In this case, you can save the overhead of generating the futures (which can be significant, depending on the number of blocks) by using `detach_loop()` instead of `submit_loop()`, with the same arguments.
+
+For example, we could detach the loop of squares example above as follows:
+
+```cpp
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include // std::setw
+#include // std::cout
+
+int main()
+{
+ BS::thread_pool pool(10);
+ constexpr unsigned int max = 100;
+ unsigned int squares[max];
+ pool.detach_loop(0, max,
+ [&squares](const unsigned int i)
+ {
+ squares[i] = i * i;
+ });
+ pool.wait();
+ for (unsigned int i = 0; i < max; ++i)
+ std::cout << std::setw(2) << i << "^2 = " << std::setw(4) << squares[i] << ((i % 5 != 4) ? " | " : "\n");
+}
+```
+
+**Warning:** Since `detach_loop()` does not return a `BS::multi_future`, there is no built-in way for the user to know when the loop finishes executing. You must use either [`wait()`](#detaching-and-waiting-for-tasks) as we did here, or some other method such as condition variables, to ensure that the loop finishes executing before trying to use anything that depends on its output. Otherwise, bad things will happen!
+
+### Parallelizing individual indices vs. blocks
+
+We have seen that `detach_loop()` and `submit_loop()` execute the function `loop(i)` for each index `i` in the loop. However, behind the scenes, the loop is split into blocks, and each block executes the `loop()` function multiple times. Each block has an internal loop of the form (where `T` is the type of the indices):
+
+```cpp
+for (T i = start; i < end; ++i)
+ loop(i);
+```
+
+The `start` and `end` indices of each block are determined automatically by the pool. For example, in the previous section, the loop from 0 to 100 was split into 10 blocks of 10 indices each: `start = 0` to `end = 10`, `start = 10` to `end = 20`, and so on; the blocks are not inclusive of the last index, since the `for` loop has the condition `i < end` and not `i <= end`.
+
+However, this also means that the `loop()` function is executed multiple times per block. This generates additional overhead due to the multiple function calls. For short loops, this should not affect performance. However, for very long loops, with millions of indices, the performance cost may be significate.
+
+For this reason, the thread pool library provides two additional member functions for parallelizing loops: `detach_blocks()` and `submit_blocks()`. While `detach_loop()` and `submit_loop()` execute a function `loop(i)` once per index but multiple times per block, `detach_blocks()` and `submit_blocks()` execute a function `block(start, end)` once per block.
+
+The main advantage of this method is increased performance, but the main disadvantage is slightly more complicated code. In particular, the user must define the loop from `start` to `end` manually within each block. Here is the previous example using `detach_blocks()`:
+
+```cpp
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include // std::setw
+#include // std::cout
+
+int main()
+{
+ BS::thread_pool pool(10);
+ constexpr unsigned int max = 100;
+ unsigned int squares[max];
+ pool.detach_blocks(0, max,
+ [&squares](const unsigned int start, const unsigned int end)
+ {
+ for (unsigned int i = start; i < end; ++i)
+ squares[i] = i * i;
+ });
+ pool.wait();
+ for (unsigned int i = 0; i < max; ++i)
+ std::cout << std::setw(2) << i << "^2 = " << std::setw(4) << squares[i] << ((i % 5 != 4) ? " | " : "\n");
+}
+```
+
+Note how the block function takes two arguments, and includes the internal loop.
+
+Generally, compiler optimizations should be able to make `detach_loop()` and `submit_loop()` perform roughly the same as `detach_blocks()` and `submit_blocks()`. However, you should perform your own benchmarks to see which option works best for your particular use case.
### Loops with return values
-Usually, `parallelize_loop()` should take functions with no return values. This is because the function will be executed once for each block, but the blocks are managed by the thread pool, so there's limited usability in returning one value per block. However, for the case where this is desired, such as for summation or some sorting algorithms, `parallelize_loop()` does accept functions with return values, in which case it returns a `BS::multi_future` object where `T` is the type of the return values.
+Unlike `submit_task()`, the member function `submit_loop()` only takes loop functions with no return values. The reason is that it wouldn't make sense to return a future for every single index of the loop. However, `submit_blocks()` does allow the block function to have a return value, as the number of blocks will generally not be too large, unlike the number of indices.
-Here's an example of summing all the numbers from 1 to 100:
+The block function will be executed once for each block, but the blocks are managed by the thread pool, with the user only able to select the number of blocks, but not the range of each block. Therefore, there is limited usability in returning one value per block. However, for cases where this is desired, such as for summation or some sorting algorithms, `submit_blocks()` does accept functions with return values, in which case it returns a `BS::multi_future` object where `T` is the type of the return values.
+
+Here's an example of a function template summing all elements of type `T` in a given range:
```cpp
-#include "BS_thread_pool.hpp"
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include // std::uint64_t
+#include // std::future
+#include // std::cout
+
+BS::thread_pool pool;
+
+template
+T sum(T min, T max)
+{
+ BS::multi_future loop_future = pool.submit_blocks(
+ min, max + 1,
+ [](const T start, const T end)
+ {
+ T block_total = 0;
+ for (T i = start; i < end; ++i)
+ block_total += i;
+ return block_total;
+ },
+ 100);
+ T result = 0;
+ for (std::future& future : loop_future)
+ result += future.get();
+ return result;
+}
+
+int main()
+{
+ std::cout << sum(1, 1'000'000);
+}
+```
+
+Here we used the fact that `BS::multi_future` is a specialization of `std::vector>`, so we can use a range-based `for` loop to iterate over the futures, and use the `get()` member function of each future to get its value. The values of the futures will be the partial sums from each block, so when we add them up, we will get the total sum. Note that we divided the loop into 100 blocks, so there will be 100 futures in total, each with the partial sum of 10,000 numbers.
+
+The range-based `for` loop will likely start before the loop finished executing, and each time it calls a future, it will get the value of that future if it is ready, or it will wait until the future is ready and then get the value. This increases performance, since we can start summing the results without waiting for the entire loop to finish executing first - we only need to wait for individual blocks.
+
+If we did want to wait until the entire loop finishes before summing the results, we could have used the `get()` member function of the `BS::multi_future` object itself, which returns an `std::vector` with the values obtained from each future. In that case, the sum could be obtained after calling `submit_blocks()` as follows:
+
+```cpp
+std::vector partial_sums = loop_future.get();
+T result = std::reduce(partial_sums.begin(), partial_sums.end());
+return result;
+```
+
+### Parallelizing sequences
+
+The member functions `detach_loop()`, `submit_loop()`, `detach_blocks()`, and `submit_blocks()` parallelize a loop by splitting it into blocks, and submitting each block as an individual task to the queue, with each such task iterating over all the indices in the corresponding block's range, which can be numerous. However, sometimes we have loops with few indices, or more generally, a sequence of tasks enumerated by some index. In such cases, we can avoid the overhead of splitting into blocks and simply submit each individual index as its own independent task to the pool's queue.
+
+This can be done with `detach_sequence()` and `submit_sequence()`. The syntax of these functions is similar to `detach_loop()` and `submit_loop()`, except that they don't have the `num_blocks` argument at the end. The sequence function must take only one argument, the index. As usual, `detach_sequence()` detaches the tasks and does not return a future, while `submit_sequence()` returns a `BS::multi_future`. If the tasks in the sequence return values, then the futures will contain those values, otherwise they will be `void` futures.
+
+Here is a simple example:
+
+```cpp
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include // std::uint64_t
+#include // std::cout
+#include // std::vector
+
+using ui64 = std::uint64_t;
+
+ui64 factorial(const ui64 n)
+{
+ ui64 result = 1;
+ for (ui64 i = 2; i <= n; ++i)
+ result *= i;
+ return result;
+}
int main()
{
BS::thread_pool pool;
- BS::multi_future mf = pool.parallelize_loop(1, 101,
- [](const int a, const int b)
- {
- int block_total = 0;
- for (int i = a; i < b; ++i)
- block_total += i;
- return block_total;
- });
- std::vector totals = mf.get();
- int sum = 0;
- for (const int t : totals)
- sum += t;
- std::cout << sum;
+ constexpr ui64 max = 20;
+ BS::multi_future sequence_future = pool.submit_sequence(0, max + 1, factorial);
+ std::vector factorials = sequence_future.get();
+ for (ui64 i = 0; i < max + 1; ++i)
+ std::cout << i << "! = " << factorials[i] << '\n';
}
```
-Calling `get()` on a `BS::multi_future` object returns an `std::vector` with the values obtained from each future. In this case, the values will be the partial sums from each block, so when we add them up, we will get the total sum.
+### More about `BS::multi_future`
-### Parallelizing loops without futures
+The helper class template `BS::multi_future`, which we have been using throughout this section, provides a convenient way to collect and access groups of futures. This class is a specialization of `std::vector`, so it should be used in a similar way:
-Just as in the case of [`push_task()`](#submitting-tasks-to-the-queue-without-futures) vs. [`submit()`](#submitting-tasks-to-the-queue-with-futures), sometimes you may want to parallelize a loop, but you don't need it to return a `BS::multi_future`. In this case, you can save the overhead of generating the futures (which can be significant, depending on the number of blocks) by using `push_loop()` instead of `parallelize_loop()`, with the same arguments.
+* When you create a new object, either use the default constructor to create an empty object and add futures to it later, or pass the desired number of futures to the constructor in advance.
+* Use the `[]` operator to access the future at a specific index, or the `push_back()` member function to append a new future to the list.
+* The `size()` member function tells you how many futures are currently stored in the object.
-For example, you could also run the loop of squares example above as follows:
+However, `BS::multi_future` also has additional member functions that are aimed specifically at handling futures:
-```cpp
-#include "BS_thread_pool.hpp"
+* Once all the futures are stored, you can use `wait()` to wait for all of them at once or `get()` to get an `std::vector` with the results from all of them.
+* You can check how many futures are ready using `ready_count()`.
+* You can check if all the stored futures are valid using `valid()`.
+* You can wait for all the stored futures for a specific duration with `wait_for()` or wait until a specific time with `wait_until()`. These functions return `true` if all futures have been waited for before the duration expired or the time point was reached, and `false` otherwise.
-int main()
-{
- BS::thread_pool pool(10);
- int squares[100];
- pool.push_loop(100,
- [&squares](const int a, const int b)
- {
- for (int i = a; i < b; ++i)
- squares[i] = i * i;
- });
- pool.wait_for_tasks();
- for (int i = 0; i < 100; ++i)
- std::cout << i << "^2 = " << squares[i] << " ";
-}
-```
+Aside from using `BS::multi_future` to track the execution of parallelized loops, it can also be used, for example, whenever you have several different groups of tasks and you want to track the execution of each group individually.
-As with `parallelize_loop()`, the first argument can be omitted if the start index is 0, and the last argument can be omitted if the number of blocks should be equal to the number of threads.
+## Utility classes
-**Warning:** Since `push_loop()` does not return a `BS::multi_future`, there is no built-in way for the user to know when the loop finishes executing. You must use either [`wait_for_tasks()`](#manually-waiting-for-all-tasks-to-complete), or some other method such as condition variables, to ensure that the loop finishes executing before trying to use anything that depends on its output. Otherwise, bad things will happen!
+The optional header file `BS_thread_pool_utils.hpp` contains several useful utility classes. These are not necessary for using the thread pool itself; `BS_thread_pool.hpp` is the only header file required. However, the utility classes can make writing multithreading code more convenient. The version of the utilities header file can be found by checking the macro `BS_THREAD_POOL_UTILS_VERSION`.
-## Helper classes
-
-### Synchronizing printing to an output stream
+### Synchronizing printing to a stream with `BS::synced_stream`
When printing to an output stream from multiple threads in parallel, the output may become garbled. For example, consider this code:
```cpp
-#include "BS_thread_pool.hpp"
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include // std::cout
+
+BS::thread_pool pool;
int main()
{
- BS::thread_pool pool;
- for (size_t i = 1; i <= 5; ++i)
- pool.push_task([i] { std::cout << "Task no. " << i << " executing.\n"; });
+ pool.detach_sequence(0, 5,
+ [](int i)
+ {
+ std::cout << "Task no. " << i << " executing.\n";
+ });
}
```
-The output may look as follows:
+The output will be a mess similar to this:
```none
-Task no. Task no. 2Task no. 5 executing.
-Task no. executing.
-Task no. 1 executing.
-4 executing.
-3 executing.
+Task no. Task no. Task no. 3 executing.
+0 executing.
+Task no. 41 executing.
+Task no. 2 executing.
+ 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.
-The helper class `BS::synced_stream` is designed to eliminate 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:
+The utility class `BS::synced_stream` is designed to eliminate 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.
@@ -758,25 +996,30 @@ The member function `print()` takes an arbitrary number of arguments, which are
As an example, this code:
```cpp
-#include "BS_thread_pool.hpp"
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include "BS_thread_pool_utils.hpp" // BS::synced_stream
+
+BS::synced_stream sync_out;
+BS::thread_pool pool;
int main()
{
- BS::synced_stream sync_out;
- BS::thread_pool pool;
- for (size_t i = 1; i <= 5; ++i)
- pool.push_task([i, &sync_out] { sync_out.println("Task no. ", i, " executing."); });
+ pool.detach_sequence(0, 5,
+ [](int i)
+ {
+ sync_out.println("Task no. ", i, " executing.");
+ });
}
```
Will print out:
```none
+Task no. 0 executing.
Task no. 1 executing.
Task no. 2 executing.
Task no. 3 executing.
Task no. 4 executing.
-Task no. 5 executing.
```
**Warning:** Always create the `BS::synced_stream` object **before** the `BS::thread_pool` object, as we did in this example. When the `BS::thread_pool` object goes out of scope, it waits for the remaining tasks to be executed. If the `BS::synced_stream` object goes out of scope before the `BS::thread_pool` object, then any tasks using the `BS::synced_stream` will crash. Since objects are destructed in the opposite order of construction, creating the `BS::synced_stream` object before the `BS::thread_pool` object ensures that the `BS::synced_stream` is always available to the tasks, even while the pool is destructing.
@@ -786,90 +1029,29 @@ Most stream manipulators defined in the headers `` and ``, such as
The only exceptions are the flushing manipulators `std::endl` and `std::flush`, which will not work because the compiler will not be able to figure out which template specializations to use. Instead, use `BS::synced_stream::endl` and `BS::synced_stream::flush`. Here is an example:
```cpp
-#include "BS_thread_pool.hpp"
-#include
-#include
-
-int main()
-{
- BS::synced_stream sync_out;
- BS::thread_pool pool;
- sync_out.print(std::setprecision(10), std::fixed);
- for (size_t i = 1; i <= 10; ++i)
- pool.push_task([i, &sync_out] { sync_out.print("The square root of ", std::setw(2), i, " is ", std::sqrt(i), ".", BS::synced_stream::endl); });
-}
-```
-
-### Handling multiple futures at once
-
-The helper class template `BS::multi_future`, already introduced in the context of `parallelize_loop()`, provides a convenient way to collect and access groups of futures. This class works similarly to STL containers such as `std::vector`:
-
-* When you create a new object, either use the default constructor to create an empty object and add futures to it later, or pass the desired number of futures to the constructor in advance.
-* Use the `[]` operator to access the future at a specific index, or the `push_back()` member function to append a new future to the list.
-* The `size()` member function tells you how many futures are currently stored in the object.
-* Once all the futures are stored, you can use `wait()` to wait for all of them at once or `get()` to get an `std::vector` with the results from all of them.
-
-Aside from using `BS::multi_future` to track the execution of parallelized loops, it can also be used whenever you have several different groups of tasks and you want to track the execution of each group individually. Here's a simple example:
-
-```cpp
-#include "BS_thread_pool.hpp"
-#include
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include "BS_thread_pool_utils.hpp" // BS::synced_stream
+#include // std::sqrt
+#include // std::setprecision, std::setw
+#include // std::fixed
BS::synced_stream sync_out;
BS::thread_pool pool;
-double power(const double i, const double j)
-{
- std::this_thread::sleep_for(std::chrono::milliseconds(10 * pool.get_thread_count()));
- return std::pow(i, j);
-}
-
-void print_vector(const std::vector& vec)
-{
- for (const double i : vec)
- sync_out.print(i, ' ');
- sync_out.println();
-}
-
int main()
{
- constexpr size_t n = 100;
-
- // First group of tasks: calculate n squares.
- // Here we create an empty BS::multi_future object, and append futures to it via push_back().
- BS::multi_future mf_squares;
- for (int i = 0; i < n; ++i)
- mf_squares.push_back(pool.submit(power, i, 2));
-
- // Second group of tasks: calculate n cubes.
- // In this case, we create a BS::multi_future object of the desired size in advance, and store the futures via the [] operator. This is faster since there will be no memory reallocations, but also more prone to errors.
- BS::multi_future mf_cubes(n);
- for (int i = 0; i < n; ++i)
- mf_cubes[i] = pool.submit(power, i, 3);
-
- // Both groups are now queued, but it will take some time until they all execute.
-
- /// ...
- /// Do some stuff while the first group of tasks executes...
- /// ...
-
- // Get and print the results from the first group.
- sync_out.println("Squares:");
- print_vector(mf_squares.get());
-
- /// ...
- /// Do other stuff while the second group of tasks executes...
- /// ...
-
- // Get and print the results from the second group.
- sync_out.println("Cubes:");
- print_vector(mf_cubes.get());
+ sync_out.print(std::setprecision(10), std::fixed);
+ pool.detach_sequence(0, 16,
+ [](int i)
+ {
+ sync_out.print("The square root of ", std::setw(2), i, " is ", std::sqrt(i), ".", BS::synced_stream::endl);
+ });
}
```
-In this example, we simulate complicated tasks by having each task wait for a bit before returning its result. We collect the futures of the tasks submitted within each group into two separate `BS::multi_future` objects. `mf_squares` holds the results from the first group, and `mf_cubes` holds the results from the second group. Now we can wait for and/or get the results from `mf_squares` whenever is convenient, and separately wait for and/or get the results from `mf_cubes` at another time.
+Note, however, that `BS::synced_stream::endl` should only be used if flushing is desired; otherwise, a newline character should be used instead.
-### Measuring execution time
+### Measuring execution time with `BS::timer`
If you are using a thread pool, then your code is most likely performance-critical. Achieving maximum performance requires performing a considerable amount of benchmarking to determine the optimal settings and algorithms. Therefore, it is important to be able to measure the execution time of various computations and operations under different conditions.
@@ -879,6 +1061,7 @@ The helper class `BS::timer` provides a simple way to measure execution time. It
2. Immediately before you execute the computation that you want to time, call the `start()` member function.
3. Immediately after the computation ends, call the `stop()` member function.
4. Use the member function `ms()` to obtain the elapsed time for the computation in milliseconds.
+5. Alternatively, use the member function `current_ms()` to obtain the elapsed time so far but keep the timer ticking.
For example:
@@ -892,7 +1075,13 @@ std::cout << "The elapsed time was " << tmr.ms() << " ms.\n";
A practical application of the `BS::timer` class can be found in the benchmark portion of the test program `BS_thread_pool_test.cpp`.
-## Other features
+### Sending simple signals between threads with `BS::signaller`
+
+`BS::signaller` is a utility class which can be used to allow simple signalling between threads. To use it, construct an object and then pass it to the different threads. Multiple threads can call the `wait()` member function of the signaller. When another thread calls the `ready()` member function, the waiting threads will stop waiting.
+
+That's really all there is to it; `BS::signaller` is really just a convenient wrapper around [`std::promise`](https://en.cppreference.com/w/cpp/thread/promise), which contains both the promise and its future. For usage examples, please see the test program `BS_thread_pool_test.cpp`.
+
+## Managing tasks
### Monitoring the tasks
@@ -906,12 +1095,15 @@ Sometimes you may wish to monitor what is happening with the tasks you submitted
These functions are demonstrated in the following program:
```cpp
-#include "BS_thread_pool.hpp"
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include "BS_thread_pool_utils.hpp" // BS::synced_stream
+#include // std::chrono
+#include // std::this_thread
BS::synced_stream sync_out;
BS::thread_pool pool(4);
-void sleep_half_second(const size_t i)
+void sleep_half_second(const int i)
{
std::this_thread::sleep_for(std::chrono::milliseconds(500));
sync_out.println("Task ", i, " done.");
@@ -924,8 +1116,8 @@ void monitor_tasks()
int main()
{
- for (size_t i = 0; i < 12; ++i)
- pool.push_task(sleep_half_second, i);
+ pool.wait();
+ pool.detach_sequence(0, 12, sleep_half_second);
monitor_tasks();
std::this_thread::sleep_for(std::chrono::milliseconds(750));
monitor_tasks();
@@ -957,173 +1149,11 @@ Task 11 done.
0 tasks total, 0 tasks running, 0 tasks queued.
```
-### Pausing the workers
-
-Sometimes you may wish to temporarily pause the execution of tasks, or perhaps you want to submit tasks to the queue in advance and only start executing them at a later time. You can do this using the member functions `pause()`, `unpause()`, and `is_paused()`.
-
-When you call `pause()`, the workers will temporarily stop retrieving new tasks out of the queue. However, any tasks already executed will keep running until they are done, since the thread pool has no control over the internal code of your tasks. If you need to pause a task in the middle of its execution, you must do that manually by programming your own pause mechanism into the task itself. To resume retrieving tasks, call `unpause()`. To check whether the pool is currently paused, call `is_paused()`.
-
-Here is an example:
-
-```cpp
-#include "BS_thread_pool.hpp"
-
-BS::synced_stream sync_out;
-BS::thread_pool pool(4);
-
-void sleep_half_second(const size_t i)
-{
- std::this_thread::sleep_for(std::chrono::milliseconds(500));
- sync_out.println("Task ", i, " done.");
-}
-
-void check_if_paused()
-{
- if (pool.is_paused())
- sync_out.println("Pool paused.");
- else
- sync_out.println("Pool unpaused.");
-}
-
-int main()
-{
- for (size_t i = 0; i < 8; ++i)
- pool.push_task(sleep_half_second, i);
- sync_out.println("Submitted 8 tasks.");
- std::this_thread::sleep_for(std::chrono::milliseconds(250));
- pool.pause();
- check_if_paused();
- std::this_thread::sleep_for(std::chrono::milliseconds(1000));
- sync_out.println("Still paused...");
- std::this_thread::sleep_for(std::chrono::milliseconds(1000));
- for (size_t i = 8; i < 12; ++i)
- pool.push_task(sleep_half_second, i);
- sync_out.println("Submitted 4 more tasks.");
- sync_out.println("Still paused...");
- std::this_thread::sleep_for(std::chrono::milliseconds(1000));
- pool.unpause();
- check_if_paused();
-}
-```
-
-Assuming you have at least 4 hardware threads, the output should be similar to:
-
-```none
-Submitted 8 tasks.
-Pool paused.
-Task 0 done.
-Task 1 done.
-Task 2 done.
-Task 3 done.
-Still paused...
-Submitted 4 more tasks.
-Still paused...
-Pool unpaused.
-Task 4 done.
-Task 5 done.
-Task 6 done.
-Task 7 done.
-Task 8 done.
-Task 9 done.
-Task 10 done.
-Task 11 done.
-```
-
-Here is what happened. We initially submitted a total of 8 tasks to the queue. Since we waited for 250ms before pausing, the first 4 tasks have already started running, so they kept running until they finished. While the pool was paused, we submitted 4 more tasks to the queue, but they just waited at the end of the queue. When we unpaused, the remaining 4 initial tasks were executed, followed by the 4 new tasks.
-
-While the workers are paused, `wait_for_tasks()` will wait for the running tasks instead of all tasks (otherwise it would wait forever). This is demonstrated by the following program:
-
-```cpp
-#include "BS_thread_pool.hpp"
-
-BS::synced_stream sync_out;
-BS::thread_pool pool(4);
-
-void sleep_half_second(const size_t i)
-{
- std::this_thread::sleep_for(std::chrono::milliseconds(500));
- sync_out.println("Task ", i, " done.");
-}
-
-void check_if_paused()
-{
- if (pool.is_paused())
- sync_out.println("Pool paused.");
- else
- sync_out.println("Pool unpaused.");
-}
-
-int main()
-{
- for (size_t i = 0; i < 8; ++i)
- pool.push_task(sleep_half_second, i);
- sync_out.println("Submitted 8 tasks. Waiting for them to complete.");
- pool.wait_for_tasks();
- for (size_t i = 8; i < 20; ++i)
- pool.push_task(sleep_half_second, i);
- sync_out.println("Submitted 12 more tasks.");
- std::this_thread::sleep_for(std::chrono::milliseconds(250));
- pool.pause();
- check_if_paused();
- sync_out.println("Waiting for the ", pool.get_tasks_running(), " running tasks to complete.");
- pool.wait_for_tasks();
- sync_out.println("All running tasks completed. ", pool.get_tasks_queued(), " tasks still queued.");
- std::this_thread::sleep_for(std::chrono::milliseconds(1000));
- sync_out.println("Still paused...");
- std::this_thread::sleep_for(std::chrono::milliseconds(1000));
- sync_out.println("Still paused...");
- std::this_thread::sleep_for(std::chrono::milliseconds(1000));
- pool.unpause();
- check_if_paused();
- std::this_thread::sleep_for(std::chrono::milliseconds(250));
- sync_out.println("Waiting for the remaining ", pool.get_tasks_total(), " tasks (", pool.get_tasks_running(), " running and ", pool.get_tasks_queued(), " queued) to complete.");
- pool.wait_for_tasks();
- sync_out.println("All tasks completed.");
-}
-```
-
-The output should be similar to:
-
-```none
-Submitted 8 tasks. Waiting for them to complete.
-Task 0 done.
-Task 1 done.
-Task 2 done.
-Task 3 done.
-Task 4 done.
-Task 5 done.
-Task 6 done.
-Task 7 done.
-Submitted 12 more tasks.
-Pool paused.
-Waiting for the 4 running tasks to complete.
-Task 8 done.
-Task 9 done.
-Task 10 done.
-Task 11 done.
-All running tasks completed. 8 tasks still queued.
-Still paused...
-Still paused...
-Pool unpaused.
-Waiting for the remaining 8 tasks (4 running and 4 queued) to complete.
-Task 12 done.
-Task 13 done.
-Task 14 done.
-Task 15 done.
-Task 16 done.
-Task 17 done.
-Task 18 done.
-Task 19 done.
-All tasks completed.
-```
-
-The first `wait_for_tasks()`, which was called while the pool was not paused, waited for all 8 tasks, both running and queued. The second `wait_for_tasks()`, which was called after pausing the pool, only waited for the 4 running tasks, while the other 8 tasks remained queued, and were not executed since the pool was paused. Finally, the third `wait_for_tasks()`, which was called after unpausing the pool, waited for the remaining 8 tasks, both running and queued.
-
-**Warning:** If the thread pool is destroyed while paused, any tasks still in the queue will never be executed!
+The reason we called `pool.wait()` in the beginning is that when the thread pool is created, an initialization task runs in each thread, so if we don't wait, the first line will say there are 16 tasks in total, including the 4 initialization tasks. See [below](#thread-pool-initialization-functions) for more details.
### Purging tasks
-Consider a situation where the user cancels a multi-threaded operation while it is still ongoing. Perhaps the operation was split into multiple tasks, and half of the tasks are currently being executed by the pool's threads, but the other half are still waiting in the queue.
+Consider a situation where the user cancels a multithreaded operation while it is still ongoing. Perhaps the operation was split into multiple tasks, and half of the tasks are currently being executed by the pool's threads, but the other half are still waiting in the queue.
The thread pool cannot terminate the tasks that are already running, as the C++17 standard does not provide that functionality (and in any case, abruptly terminating a task while it's running could have extremely bad consequences, such as memory leaks and data corruption). However, the tasks that are still waiting in the queue can be purged using the `purge()` member function.
@@ -1132,7 +1162,10 @@ Once `purge()` is called, any tasks still waiting in the queue will be discarded
Consider for example the following program:
```cpp
-#include "BS_thread_pool.hpp"
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include "BS_thread_pool_utils.hpp" // BS::synced_stream
+#include // std::chrono
+#include // std::this_thread
BS::synced_stream sync_out;
BS::thread_pool pool(4);
@@ -1141,7 +1174,7 @@ int main()
{
for (size_t i = 0; i < 8; ++i)
{
- pool.push_task(
+ pool.detach_task(
[i]
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
@@ -1150,7 +1183,7 @@ int main()
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
pool.purge();
- pool.wait_for_tasks();
+ pool.wait();
}
```
@@ -1165,7 +1198,7 @@ Task 3 done.
### Exception handling
-`submit()` catches any exceptions thrown by the submitted task and forwards them to the corresponding future. They can then be caught when invoking the `get()` member function of the future. For example:
+`submit_task()` catches any exceptions thrown by the submitted task and forwards them to the corresponding future. They can then be caught when invoking the `get()` member function of the future. For example:
```cpp
#include "BS_thread_pool.hpp"
@@ -1184,7 +1217,7 @@ double inverse(const double x)
int main()
{
constexpr double num = 0;
- std::future my_future = pool.submit(inverse, num);
+ std::future my_future = pool.submit_task(inverse, num);
try
{
const double result = my_future.get();
@@ -1224,7 +1257,7 @@ void print_inverse(const double x)
int main()
{
constexpr double num = 0;
- std::future my_future = pool.submit(print_inverse, num);
+ std::future my_future = pool.submit_task(print_inverse, num);
try
{
my_future.get();
@@ -1238,75 +1271,703 @@ int main()
When using `BS::multi_future` to handle multiple futures at once, exception handling works the same way: if any of the futures may throw exceptions, you may catch these exceptions when calling `get()`, even in the case of `BS::multi_future`.
-## Testing the package
+### Getting information about the threads
+
+`BS::thread_pool` comes with a variety of methods to obtain information about the threads in the pool:
+
+1. The namespace `BS::this_thread` provides functionality similar to `std::this_thread`. If the current thread belongs to a `BS::thread_pool` object, then `BS::this_thread::get_index()` can be used to get the index of the current thread, and `BS::this_thread::get_pool()` can be used to get the pointer to the thread pool that owns the current thread. Please see [the reference below](#the-bsthis_thread-namespace) for more details.
+2. The member function `get_thread_ids()` returns a vector containing the unique identifiers for each of the pool's threads, as obtained by `std::thread::get_id()`. These values are not so useful on their own, but can be used for whatever the user wants to use them for.
+3. The optional member function `get_native_handles()`, if enabled, returns a vector containing the underlying implementation-defined thread handles for each of the pool's threads, as obtained by `std::thread::native_handle()`. For more information, see [the relevant section below](#accessing-native-thread-handles).
+
+### Thread pool initialization functions
+
+Sometimes, it is necessary to initialize the threads before they run any tasks. This can be done by submitting a proper initialization function to the constructor or to `reset()`, either as the only argument or as the second argument after the desired number of threads. The thread initialization must take no arguments and have no return value. However, if needed, the function can use `BS::this_thread::get_index()` and `BS::this_thread::get_pool()` to figure out which thread and pool it belongs to.
+
+The thread initialization function is submitted as a set of special tasks, one per thread, which bypass the queue, but still count towards the number of running tasks, which means `get_tasks_total()` and `get_tasks_running()` will report that these tasks are running if they are checked immediately after the pool is initialized.
+
+This is done so that the user has the option to either wait for the initialization tasks to finish, by calling `wait()` on the pool, or just keep going. In either case, the initialization tasks will **always** finish executing before any tasks are picked out of the queue, so there is no reason to wait for them to finish unless they have some side-effects that affect the main thread.
+
+Here is a simple example:
+
+```cpp
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include "BS_thread_pool_utils.hpp" // BS::synced_stream
+#include // std::mt19937_64, std::random_device
+
+BS::synced_stream sync_out;
+thread_local std::mt19937_64 twister;
+
+int main()
+{
+ BS::thread_pool pool(
+ []
+ {
+ twister.seed(std::random_device()());
+ });
+ pool.submit_sequence(0, 4,
+ [](int)
+ {
+ sync_out.println("I generated a random number: ", twister());
+ })
+ .wait();
+}
+```
+
+In this example, we create a `thread_local` Mersenne twister engine, meaning that each thread has its own independent engine. However, we did not seed the engine, so each thread will generate the exact same sequence of pseudo-random numbers. To remedy this, we pass an initialization function to the `BS::thread_pool` constructor which seeds the twister in each thread with the (hopefully) non-deterministic random number generator `std::random_device`.
+
+### Passing task arguments by constant reference
+
+In C++, it is often crucial to pass function arguments by reference or constant reference, instead of by value. This allows the function to access the object being passed directly, rather than creating a new copy of the object. We have already seen that submitting an argument by reference is a simple matter of capturing it with a `&` in the lambda capture list. To submit as constant reference, we can use `std::as_const` as in the following example:
+
+```cpp
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include "BS_thread_pool_utils.hpp" // BS::synced_stream
+#include // std::as_const
+
+BS::synced_stream sync_out;
+
+void increment(int& x)
+{
+ ++x;
+}
+
+void print(const int& x)
+{
+ sync_out.println(x);
+}
+
+int main()
+{
+ BS::thread_pool pool;
+ int n = 0;
+ pool.submit_task(
+ [&n]
+ {
+ increment(n);
+ })
+ .wait();
+ pool.submit_task(
+ [&n = std::as_const(n)]
+ {
+ print(n);
+ })
+ .wait();
+}
+```
+
+The `increment()` function takes a **reference** to an integer, and increments that integer. Passing the argument by reference guarantees that `n` itself, in the scope of `main()`, will be incremented - rather than a copy of it in the scope of `increment()`.
+
+Similarly, the `print()` function takes a **constant reference** to an integer, and prints that integer. Passing the argument by constant reference guarantees that the variable will not be accidentally modified by the function, even though we are accessing `n` itself, rather than a copy. If we replace `print` with `increment`, the program won't compile, as `increment` cannot take constant references.
+
+Generally, it is not really necessary to pass arguments by constant reference, but it is more "correct" to do so, if we would like to guarantee that the variable being referenced is indeed never modified. This section is therefore included here for completeness.
+
+## Optional features
+
+### Pausing the pool
+
+Sometimes you may wish to temporarily pause the execution of tasks, or perhaps you want to submit tasks to the queue in advance and only start executing them at a later time. You can do this using the member functions `pause()`, `unpause()`, and `is_paused()`.
+
+However, these functions are disabled by default, and must be explicitly enabled by defining the macro `BS_THREAD_POOL_ENABLE_PAUSE` before including `BS_thread_pool.hpp`. The reason is that pausing the pool adds additional checks to the waiting and worker functions, which have a very small but non-zero overhead.
+
+When you call `pause()`, the workers will temporarily stop retrieving new tasks out of the queue. However, any tasks already executed will keep running until they are done, since the thread pool has no control over the internal code of your tasks. If you need to pause a task in the middle of its execution, you must do that manually by programming your own pause mechanism into the task itself. To resume retrieving tasks, call `unpause()`. To check whether the pool is currently paused, call `is_paused()`.
+
+Here is an example:
+
+```cpp
+#define BS_THREAD_POOL_ENABLE_PAUSE
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include "BS_thread_pool_utils.hpp" // BS::synced_stream
+#include // std::chrono
+#include // std::this_thread
+
+BS::synced_stream sync_out;
+BS::thread_pool pool(4);
+
+void sleep_half_second(const int i)
+{
+ std::this_thread::sleep_for(std::chrono::milliseconds(500));
+ sync_out.println("Task ", i, " done.");
+}
+
+void check_if_paused()
+{
+ if (pool.is_paused())
+ sync_out.println("Pool paused.");
+ else
+ sync_out.println("Pool unpaused.");
+}
+
+int main()
+{
+ pool.detach_sequence(0, 8, sleep_half_second);
+ sync_out.println("Submitted 8 tasks.");
+ std::this_thread::sleep_for(std::chrono::milliseconds(250));
+ pool.pause();
+ check_if_paused();
+ std::this_thread::sleep_for(std::chrono::milliseconds(1000));
+ sync_out.println("Still paused...");
+ std::this_thread::sleep_for(std::chrono::milliseconds(1000));
+ pool.detach_sequence(8, 12, sleep_half_second);
+ sync_out.println("Submitted 4 more tasks.");
+ sync_out.println("Still paused...");
+ std::this_thread::sleep_for(std::chrono::milliseconds(1000));
+ pool.unpause();
+ check_if_paused();
+}
+```
+
+Assuming you have at least 4 hardware threads, the output should be similar to:
+
+```none
+Submitted 8 tasks.
+Pool paused.
+Task 0 done.
+Task 1 done.
+Task 2 done.
+Task 3 done.
+Still paused...
+Submitted 4 more tasks.
+Still paused...
+Pool unpaused.
+Task 4 done.
+Task 5 done.
+Task 6 done.
+Task 7 done.
+Task 8 done.
+Task 9 done.
+Task 10 done.
+Task 11 done.
+```
+
+Here is what happened. We initially submitted a total of 8 tasks to the queue. Since we waited for 250ms before pausing, the first 4 tasks have already started running, so they kept running until they finished. While the pool was paused, we submitted 4 more tasks to the queue, but they just waited at the end of the queue. When we unpaused, the remaining 4 initial tasks were executed, followed by the 4 new tasks.
+
+While the workers are paused, `wait()` will wait for the running tasks instead of all tasks (otherwise it would wait forever). This is demonstrated by the following program:
+
+```cpp
+#define BS_THREAD_POOL_ENABLE_PAUSE
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include "BS_thread_pool_utils.hpp" // BS::synced_stream
+#include // std::chrono
+#include // std::this_thread
+
+BS::synced_stream sync_out;
+BS::thread_pool pool(4);
+
+void sleep_half_second(const int i)
+{
+ std::this_thread::sleep_for(std::chrono::milliseconds(500));
+ sync_out.println("Task ", i, " done.");
+}
+
+void check_if_paused()
+{
+ if (pool.is_paused())
+ sync_out.println("Pool paused.");
+ else
+ sync_out.println("Pool unpaused.");
+}
+
+int main()
+{
+ pool.detach_sequence(0, 8, sleep_half_second);
+ sync_out.println("Submitted 8 tasks. Waiting for them to complete.");
+ pool.wait();
+ pool.detach_sequence(8, 20, sleep_half_second);
+ sync_out.println("Submitted 12 more tasks.");
+ std::this_thread::sleep_for(std::chrono::milliseconds(250));
+ pool.pause();
+ check_if_paused();
+ sync_out.println("Waiting for the ", pool.get_tasks_running(), " running tasks to complete.");
+ pool.wait();
+ sync_out.println("All running tasks completed. ", pool.get_tasks_queued(), " tasks still queued.");
+ std::this_thread::sleep_for(std::chrono::milliseconds(1000));
+ sync_out.println("Still paused...");
+ std::this_thread::sleep_for(std::chrono::milliseconds(1000));
+ sync_out.println("Still paused...");
+ std::this_thread::sleep_for(std::chrono::milliseconds(1000));
+ pool.unpause();
+ check_if_paused();
+ std::this_thread::sleep_for(std::chrono::milliseconds(250));
+ sync_out.println("Waiting for the remaining ", pool.get_tasks_total(), " tasks (", pool.get_tasks_running(), " running and ", pool.get_tasks_queued(), " queued) to complete.");
+ pool.wait();
+ sync_out.println("All tasks completed.");
+}
+```
+
+The output should be similar to:
+
+```none
+Submitted 8 tasks. Waiting for them to complete.
+Task 0 done.
+Task 1 done.
+Task 2 done.
+Task 3 done.
+Task 4 done.
+Task 5 done.
+Task 6 done.
+Task 7 done.
+Submitted 12 more tasks.
+Pool paused.
+Waiting for the 4 running tasks to complete.
+Task 8 done.
+Task 9 done.
+Task 10 done.
+Task 11 done.
+All running tasks completed. 8 tasks still queued.
+Still paused...
+Still paused...
+Pool unpaused.
+Waiting for the remaining 8 tasks (4 running and 4 queued) to complete.
+Task 12 done.
+Task 13 done.
+Task 14 done.
+Task 15 done.
+Task 16 done.
+Task 17 done.
+Task 18 done.
+Task 19 done.
+All tasks completed.
+```
+
+The first `wait()`, which was called while the pool was not paused, waited for all 8 tasks, both running and queued. The second `wait()`, which was called after pausing the pool, only waited for the 4 running tasks, while the other 8 tasks remained queued, and were not executed since the pool was paused. Finally, the third `wait()`, which was called after unpausing the pool, waited for the remaining 8 tasks, both running and queued.
+
+**Warning:** If the thread pool is destroyed while paused, any tasks still in the queue will never be executed!
+
+### Avoiding wait deadlocks
+
+Consider the following program:
+
+```cpp
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include // std::cout
+
+int main()
+{
+ BS::thread_pool pool;
+ pool.detach_task(
+ [&pool]
+ {
+ pool.wait();
+ std::cout << "Done waiting.\n";
+ });
+}
+```
+
+This program creates a thread pool, and then detaches a task that waits for tasks in the same thread pool to complete. If you run this program, it will never print the message "Done waiting", because the task will wait for **itself** to complete. This causes a **deadlock**, and the program will wait forever.
+
+Usually, in simple programs, this will never happen. However, in more complicated programs, perhaps ones running multiple thread pools in parallel, wait deadlocks could potentially occur. In such cases, the macro `BS_THREAD_POOL_ENABLE_WAIT_DEADLOCK_CHECK` can be defined before including `BS_thread_pool.hpp`. `wait()` will then check whether the user tried to call it from within a thread of the same pool, and if so, it will throw the exception `BS::thread_pool::wait_deadlock` instead of waiting. This check is disabled by default because wait deadlocks are not something that happens often, and the check adds a small but non-zero overhead every time `wait()` is called.
+
+Here is an example:
+
+```cpp
+#define BS_THREAD_POOL_ENABLE_WAIT_DEADLOCK_CHECK
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include // std::cout
+
+int main()
+{
+ BS::thread_pool pool;
+ pool.detach_task(
+ [&pool]
+ {
+ try
+ {
+ pool.wait();
+ std::cout << "Done waiting.\n";
+ }
+ catch (const BS::thread_pool::wait_deadlock&)
+ {
+ std::cout << "Error: Deadlock!\n";
+ }
+ });
+}
+```
+
+This time, `wait()` will detect the deadlock, and will throw an exception, causing the output to be `"Error: Deadlock!"`.
+
+### Accessing native thread handles
+
+The `BS::thread_pool` member function `get_native_handles()` returns a vector containing the underlying implementation-defined thread handles for each of the pool's threads. These can then be used in an implementation-specific way to manage the threads at the OS level
+
+However, note that this will generally **not** be portable code. Furthermore, this feature uses [std::thread::native_handle()](https://en.cppreference.com/w/cpp/thread/thread/native_handle), which is in the C++ standard library, but is **not** guaranteed to be present on all systems. Therefore, this feature is turned off by default, and must be turned on by defining the macro `BS_THREAD_POOL_ENABLE_NATIVE_HANDLES` before including `BS_thread_pool.hpp`.
+
+Here is an example:
+
+```cpp
+#define BS_THREAD_POOL_ENABLE_NATIVE_HANDLES
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include "BS_thread_pool_utils.hpp" // BS::synced_stream
+#include // std::thread
+#include // std::vector
+
+BS::synced_stream sync_out;
+BS::thread_pool pool(4);
+
+int main()
+{
+ std::vector handles = pool.get_native_handles();
+ for (BS::concurrency_t i = 0; i < handles.size(); ++i)
+ sync_out.println("Thread ", i, " native handle: ", handles[i]);
+}
+```
+
+The output will depend on your compiler and operating system. Here is an example:
+
+```none
+Thread 0 native handle: 000000F4
+Thread 1 native handle: 000000F8
+Thread 2 native handle: 000000EC
+Thread 3 native handle: 000000FC
+```
+
+### Setting task priority
+
+Defining the macro `BS_THREAD_POOL_ENABLE_PRIORITY` before including `BS_thread_pool.hpp` enables task priority. The priority of a task or group of tasks may then be specified as an additional argument (at the end of the argument list) to `detach_task()`, `submit_task()`, `detach_blocks()`, `submit_blocks()`, `detach_loop()`, `submit_loop()`, `detach_sequence()`, and `submit_sequence()`. If the priority is not specified, the default value will be 0.
+
+The priority is a number of type `BS::priority_t`, which is a signed 16-bit integer, so it can have any value between -32,768 and 32,767. The tasks will be executed in priority order from highest to lowest. If priority is assigned to the block/loop/sequence parallelization functions, which submit multiple tasks, then all of these tasks will have the same priority.
+
+The namespace `BS::pr` contains some pre-defined priorities for users who wish to avoid magic numbers and enjoy better future-proofing. In order of decreasing priority, the pre-defined priorities are: `BS::pr::highest`, `BS::pr::high`, `BS::pr::normal`, `BS::pr::low`, and `BS::pr::lowest`.
+
+Here is a simple example:
+
+```cpp
+#define BS_THREAD_POOL_ENABLE_PRIORITY
+#include "BS_thread_pool.hpp" // BS::thread_pool
+#include "BS_thread_pool_utils.hpp" // BS::synced_stream
+
+BS::synced_stream sync_out;
+BS::thread_pool pool(1);
+
+int main()
+{
+ pool.detach_task( [] { sync_out.println("This task will execute third."); }, BS::pr::normal);
+ pool.detach_task( [] { sync_out.println("This task will execute fifth."); }, BS::pr::lowest);
+ pool.detach_task( [] { sync_out.println("This task will execute second."); }, BS::pr::high);
+ pool.detach_task( [] { sync_out.println("This task will execute first."); }, BS::pr::highest);
+ pool.detach_task( [] { sync_out.println("This task will execute fourth."); }, BS::pr::low);
+}
+```
+
+This program will print out the tasks in the correct priority order. Note that for simplicity, we used a pool with just one thread, so the tasks will run one at a time. In a pool with 5 or more threads, all 5 tasks will actually run more or less at the same time, because, for example, the task with the second-highest priority will be picked up by another thread while the task with the highest priority is still running.
+
+Of course, this is just a pedagogical example. In a realistic use case we may want, for example, to submit tasks that must be completed immediately with high priority so they skip over other tasks already in the queue, or background non-urgent tasks with low priority so they evaluate only after higher-priority tasks are done.
+
+Here are some subtleties to note when using task priority:
+
+* Task priority is facilitated using [`std::priority_queue`](https://en.cppreference.com/w/cpp/container/priority_queue), which has O(log n) complexity for storing new tasks, but only O(1) complexity for retrieving the next (i.e. highest-priority) task. This is in contrast with [`std::queue`](https://en.cppreference.com/w/cpp/container/queue), used if priority is disabled, which both stores and retrieves with O(1) complexity.
+* Due to this, enabling the priority queue can incur a very slight decrease in performance, depending on the specific use case, which is why this feature is disabled by default. As usual, there is a trade-off here, where you get functionality in exchange for performance. However, the difference in performance is never substantial, and compiler optimizations can often reduce it to a negligible amount.
+* When using the priority queue, tasks will not necessarily be executed in the same order they were submitted, **even if they all have the same priority**. This is due to the implementation of `std::priority_queue` as a [binary heap](https://en.wikipedia.org/wiki/Binary_heap), which means tasks are stored as a binary tree instead of sequentially. To execute tasks in submission order, give them monotonically decreasing priorities.
+* Technically, `BS::priority_t` is defined to be ([`std::int_least16_t`](https://en.cppreference.com/w/cpp/types/integer)), since this type is guaranteed to be present on all systems, rather than `std::int16_t`, which is optional in the C++ standard. This means that on some exotic systems `BS::priority_t` may actually have more than 16 bits. However, the pre-defined priorities are 100% portable, and will always have the same values (e.g.: `BS::pr::highest = 32767`) regardless of the actual bit width.
+
+## Testing the library
### Automated tests
-The file `BS_thread_pool_test.cpp` in the `tests` folder of the GitHub repository will perform automated tests of all aspects of the package. The output will be printed both to `std::cout` and to a file named `BS_thread_pool_test-yyyy-mm-dd_hh.mm.ss.log` based on the current date and time. In addition, the code is thoroughly documented, and is meant to serve as an extensive example of how to properly use the package.
+The file `BS_thread_pool_test.cpp` in the `tests` folder of the GitHub repository will perform automated tests of all aspects of the library. The output will be printed both to `std::cout` and to a file with the same name as the executable and the suffix `-yyyy-mm-dd_hh.mm.ss.log` based on the current date and time. In addition, the code is meant to serve as an extensive example of how to properly use the library.
Please make sure to:
1. [Compile](#compiling-and-compatibility) `BS_thread_pool_test.cpp` with optimization flags enabled (e.g. `-O3` on GCC / Clang or `/O2` on MSVC).
2. Run the test without any other applications, especially multithreaded applications, running in parallel.
-A PowerShell script, `BS_thread_pool_test.ps1`, is provided for your convenience in the `tests` folder to make running the test on multiple compilers and operating systems easier. Since it is written in PowerShell, it is fully portable and works on Windows, Linux, and macOS. The script will automatically detect if Clang, GCC, and/or MSVC are available, compile the test program using each available compiler, and then run each compiled test program 5 times and report on any errors.
+The test program also takes command line arguments for automation purposes:
+
+* `help`: Show a help message and exit. Any other arguments will be ignored.
+* `log`: Create a log file.
+* `tests`: Perform standard tests.
+* `deadlock` Perform long deadlock tests.
+* `benchmarks`: Perform benchmarks.
+
+If no options are entered, the default is: `log tests benchmarks`.
+
+By default, the test program enables all the optional features by defining the suitable macros, so it can test them. However, if the macro `BS_THREAD_POOL_LIGHT_TEST` is defined during compilation, the optional features will not be tested.
+
+A PowerShell script, `BS_thread_pool_test.ps1`, is provided for your convenience in the `tests` folder to make running the test on multiple compilers and operating systems easier. Since it is written in PowerShell, it is fully portable and works on Windows, Linux, and macOS. The script will automatically detect if Clang, GCC, and/or MSVC are available, and compile the test program using each available compiler twice - with and without all the optional features. It will then run each compiled test program and report on any errors.
If any of the tests fail, please [submit a bug report](https://github.com/bshoshany/thread-pool/issues) including the exact specifications of your system (OS, CPU, compiler, etc.) and the generated log file.
### Performance tests
-If all checks passed, `BS_thread_pool_test.cpp` performs simple benchmarks by filling a specific number of vectors of fixed size with values. The program decides how many vectors to use, and of what size, by testing how many are needed to reach a certain target duration in a single-threaded computation. This ensures that the test takes approximately the same amount of time on all systems, and is thus more consistent and portable.
+If all checks passed, `BS_thread_pool_test.cpp` performs simple benchmarks by filling a very large vector with values using `detach_blocks()`. The program decides what the size of the vector should be by testing how many elements are needed to reach a certain target duration when parallelizing using a number of blocks equal to the number of threads. This ensures that the test takes approximately the same amount of time on all systems, and is thus more consistent and portable.
-Once the appropriate number and size of vectors has been determined, the program allocates the vectors and fills them with values, calculated according to a fixed prescription. This operation is performed both single-threaded and multithreaded, with the multithreaded computation spread across multiple tasks submitted to the pool.
+Once the appropriate size of the vector has been determined, the program allocates the vector and fills it with values, calculated according to a fixed prescription. This operation is performed both single-threaded and multithreaded, with the multithreaded computation spread across multiple tasks submitted to the pool.
-Several different multithreaded tests are performed, with the number of tasks either equal to, smaller than, or larger than the pool's thread count. Each test is repeated multiple times, with the run times averaged over all runs of the same test. The run times of the tests are compared, and the maximum speedup obtained is calculated.
+Several different multithreaded tests are performed, with the number of tasks either equal to, smaller than, or larger than the pool's thread count. Each test is repeated multiple times, with the run times averaged over all runs of the same test. The program keeps increasing the number of blocks by a factor of 2 until diminishing returns are encountered. The run times of the tests are compared, and the maximum speedup obtained is calculated.
-As an example, here are the results of the benchmarks from a [Digital Research Alliance of Canada](https://alliancecan.ca/en) node equipped with two 20-core / 40-thread Intel Xeon Gold 6148 CPUs (for a total of 40 cores and 80 threads), running CentOS Linux 7.9.2009. The tests were compiled using GCC v12.2.0 with the `-O3` and `-march=native` flags. The output was as follows:
+As an example, here are the results of the benchmarks from a [Digital Research Alliance of Canada](https://alliancecan.ca/en) node equipped with two 20-core / 40-thread Intel Xeon Gold 6148 CPUs (for a total of 40 cores and 80 threads), running CentOS Linux 7.9.2009. The tests were compiled using GCC v13.2.0 with the `-O3` and `-march=native` flags. The output was as follows:
```none
======================
Performing benchmarks:
======================
Using 80 threads.
-Each test will be repeated 20 times to collect reliable statistics.
-Determining the number and size of vectors to generate in order to achieve an approximate mean execution time of 50 ms with 80 tasks...
-Generating 3840 vectors with 5120 elements each:
-Single-threaded, mean execution time was 2122.5 ms with standard deviation 18.1 ms.
-With 20 tasks, mean execution time was 119.8 ms with standard deviation 13.3 ms.
-With 40 tasks, mean execution time was 68.9 ms with standard deviation 0.3 ms.
-With 80 tasks, mean execution time was 45.5 ms with standard deviation 6.2 ms.
-With 160 tasks, mean execution time was 39.3 ms with standard deviation 3.3 ms.
-With 320 tasks, mean execution time was 40.4 ms with standard deviation 2.4 ms.
-Maximum speedup obtained by multithreading vs. single-threading: 53.9x, using 160 tasks.
+Determining the number of elements to generate in order to achieve an approximate mean execution time of 50 ms with 80 tasks...
+Each test will be repeated up to 30 times to collect reliable statistics.
+Generating 27962000 elements:
+[......]
+Single-threaded, mean execution time was 2815.2 ms with standard deviation 3.5 ms.
+[......]
+With 2 tasks, mean execution time was 1431.3 ms with standard deviation 10.1 ms.
+[.......]
+With 4 tasks, mean execution time was 722.1 ms with standard deviation 11.4 ms.
+[..............]
+With 8 tasks, mean execution time was 364.9 ms with standard deviation 10.9 ms.
+[............................]
+With 16 tasks, mean execution time was 181.9 ms with standard deviation 8.0 ms.
+[..............................]
+With 32 tasks, mean execution time was 110.6 ms with standard deviation 1.8 ms.
+[..............................]
+With 64 tasks, mean execution time was 64.0 ms with standard deviation 6.3 ms.
+[..............................]
+With 128 tasks, mean execution time was 59.8 ms with standard deviation 0.8 ms.
+[..............................]
+With 256 tasks, mean execution time was 59.0 ms with standard deviation 0.0 ms.
+[..............................]
+With 512 tasks, mean execution time was 52.8 ms with standard deviation 0.4 ms.
+[..............................]
+With 1024 tasks, mean execution time was 50.7 ms with standard deviation 0.9 ms.
+[..............................]
+With 2048 tasks, mean execution time was 50.0 ms with standard deviation 0.5 ms.
+[..............................]
+With 4096 tasks, mean execution time was 49.4 ms with standard deviation 0.5 ms.
+[..............................]
+With 8192 tasks, mean execution time was 50.2 ms with standard deviation 0.4 ms.
+Maximum speedup obtained by multithreading vs. single-threading: 56.9x, using 4096 tasks.
+++++++++++++++++++++++++++++++++++++++
Thread pool performance test completed!
+++++++++++++++++++++++++++++++++++++++
```
-These two CPUs have 40 physical cores in total, with each core providing two separate logical cores via hyperthreading, for a total of 80 threads. Without hyperthreading, we would expect a maximum theoretical speedup of 40x. With hyperthreading, one might naively expect to achieve up to an 80x speedup, but this is in fact impossible, as each pair of hyperthreaded logical cores share the same physical core's resources. However, generally we would expect at most an estimated 30% additional speedup from hyperthreading, which amounts to around 52x in this case. The speedup of 53.9x in our performance test exceeds this estimate.
+These two CPUs have 40 physical cores in total, with each core providing two separate logical cores via hyperthreading, for a total of 80 threads. Without hyperthreading, we would expect a maximum theoretical speedup of 40x. With hyperthreading, one might naively expect to achieve up to an 80x speedup, but this is in fact impossible, as each pair of hyperthreaded logical cores share the same physical core's resources. However, generally we would expect at most an estimated 30% additional speedup from hyperthreading, which amounts to around 52x in this case. The speedup of 56.9x in our performance test exceeds this estimate.
-## The light version of the package
+## Installing the library using package managers
-This package started out as a very lightweight C++ thread pool, but over time has expanded to include many additional features and helper classes. Therefore, I have decided to bundle a light version of the thread pool in a separate and stand-alone header file, `BS_thread_pool_light.hpp`.
+### Installing using vcpkg
-This file does not contain any of the helper classes, only a new `BS::thread_pool_light` class, which is a minimal thread pool with only the 5 most basic member functions:
+If you are using the [vcpkg](https://vcpkg.io/) C/C++ package manager, you can easily install `BS::thread_pool` with the following commands:
-* `get_thread_count()`
-* `push_loop()`
-* `push_task()`
-* `submit()`
-* `wait_for_tasks()`
+On Linux/macOS:
-Note that each header file is 100% stand-alone. If you wish to use the full package, you only need `BS_thread_pool.hpp`, and if you wish to use the light version, you only need `BS_thread_pool_light.hpp`. Only a single header file needs to be included in your project. However, if you wish to use both the light and non-light thread pool classes in the same project, you can include both header files.
+```none
+./vcpkg install bshoshany-thread-pool
+```
-The test program `BS_thread_pool_test.cpp` tests both the full and the light versions of the package. If needed, the current version of the light thread pool can be obtained using the macro `BS_THREAD_POOL_LIGHT_VERSION`.
+On Windows:
+
+```none
+.\vcpkg install bshoshany-thread-pool:x86-windows bshoshany-thread-pool:x64-windows
+```
+
+To update the package to the latest version, run:
+
+```none
+vcpkg upgrade
+```
+
+### Installing using Conan
+
+If you are using the [Conan](https://conan.io/) C/C++ package manager, you can easily integrate `BS::thread_pool` into your project by adding the following lines to your `conanfile.txt`:
+
+```ini
+[requires]
+bshoshany-thread-pool/4.0.0
+```
+
+To update the package to the latest version, simply change the version number. Please refer to [this package's page on ConanCenter](https://conan.io/center/recipes/bshoshany-thread-pool) for more information.
+
+### Installing using Meson
+
+If you are using the [Meson](https://mesonbuild.com/) build system, you can install `BS::thread_pool` from [WrapDB](https://mesonbuild.com/Wrapdb-projects.html). To do so, create a `subprojects` folder in your project (if it does not already exist) and run the following command:
+
+```none
+meson wrap install bshoshany-thread-pool
+```
+
+Then, use `dependency('bshoshany-thread-pool')` in your `meson.build` file to include the package. To update the package to the latest version, run:
+
+```none
+meson wrap update bshoshany-thread-pool
+```
+
+### Installing using CMake with CPM
+
+If you are using [CMake](https://cmake.org/), you can install `BS::thread_pool` with [CPM](https://github.com/cpm-cmake/CPM.cmake). If CPM is already installed, simply add the following to your project's `CMakeLists.txt`:
+
+```cmake
+CPMAddPackage(
+ NAME BS_thread_pool
+ GITHUB_REPOSITORY bshoshany/thread-pool
+ VERSION 4.0.0)
+add_library(BS_thread_pool INTERFACE)
+target_include_directories(BS_thread_pool INTERFACE ${BS_thread_pool_SOURCE_DIR}/include)
+```
+
+This will automatically download the indicated version of the package from this GitHub repository and include it in your project.
+
+It is also possible to use CPM without installing it first, by adding the following lines to `CMakeLists.txt` before `CPMAddPackage`:
+
+```cmake
+set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM.cmake")
+if(NOT(EXISTS ${CPM_DOWNLOAD_LOCATION}))
+ message(STATUS "Downloading CPM.cmake")
+ file(DOWNLOAD https://github.com/cpm-cmake/CPM.cmake/releases/latest/download/CPM.cmake ${CPM_DOWNLOAD_LOCATION})
+endif()
+include(${CPM_DOWNLOAD_LOCATION})
+```
+
+Here is an example of a complete `CMakeLists.txt` for a project named `my_project` consisting of a single source file `main.cpp` which uses `BS_thread_pool.hpp`:
+
+```cmake
+cmake_minimum_required(VERSION 3.19)
+project(my_project LANGUAGES CXX)
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_CXX_EXTENSIONS OFF)
+set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM.cmake")
+if(NOT(EXISTS ${CPM_DOWNLOAD_LOCATION}))
+ message(STATUS "Downloading CPM.cmake")
+ file(DOWNLOAD https://github.com/cpm-cmake/CPM.cmake/releases/latest/download/CPM.cmake ${CPM_DOWNLOAD_LOCATION})
+endif()
+include(${CPM_DOWNLOAD_LOCATION})
+CPMAddPackage(
+ NAME BS_thread_pool
+ GITHUB_REPOSITORY bshoshany/thread-pool
+ VERSION 4.0.0)
+add_library(BS_thread_pool INTERFACE)
+target_include_directories(BS_thread_pool INTERFACE ${BS_thread_pool_SOURCE_DIR}/include)
+add_executable(my_project main.cpp)
+target_link_libraries(my_project BS_thread_pool)
+```
+
+With both `CMakeLists.txt` and `main.cpp` in the same folder, type the following commands to build the project:
+
+```none
+cmake -S . -B build
+cmake --build build
+```
+
+## Complete library reference
+
+This section provides a complete reference to classes, member functions, objects, and macros available in this library, along with other important information. Member functions are given here with simplified prototypes (e.g. removing `const`) for ease of reading.
+
+More information can be found in the provided [Doxygen](https://www.doxygen.nl/) comments. Any modern IDE, such as [Visual Studio Code](https://code.visualstudio.com/), can use the Doxygen comments to provide automatic documentation for any class and member function in this library when hovering over code with the mouse or using auto-complete.
+
+### Main thread pool header file (`BS_thread_pool.hpp`)
+
+#### The `BS::thread_pool` class
+
+The class `BS::thread_pool` is the main thread pool class. It can be used to create a pool of threads and submit tasks to a queue. When a thread becomes available, it takes a task from the queue and executes it. The member functions that are available by default, when no macros are defined, are:
+
+* Constructors:
+ * `thread_pool()`: Construct a new thread pool. The number of threads will be the total number of hardware threads available, as reported by the implementation. This is usually determined by the number of cores in the CPU. If a core is hyperthreaded, it will count as two threads.
+ * `thread_pool(BS::concurrency_t num_threads)`: Construct a new thread pool with the specified number of threads.
+ * `thread_pool(std::function& init_task)`: Construct a new thread pool with the specified initialization function.
+ * `thread_pool(BS::concurrency_t num_threads, std::function& init_task)`: Construct a new thread pool with the specified number of threads and initialization function.
+* Resetters:
+ * `void reset()`: Reset the pool with the total number of hardware threads available, as reported by the implementation. Waits for all currently running tasks to be completed, then destroys all threads in the pool and creates a new thread pool with the new number of threads. Any tasks that were waiting in the queue before the pool was reset will then be executed by the new threads. If the pool was paused before resetting it, the new pool will be paused as well.
+ * `void reset(BS::concurrency_t num_threads)`: Reset the pool with a new number of threads.
+ * `void reset(std::function& init_task)` Reset the pool with the total number of hardware threads available, as reported by the implementation, and a new initialization function.
+ * `void reset(BS::concurrency_t num_threads, std::function& init_task)`: Reset the pool with a new number of threads and a new initialization function.
+* Getters:
+ * `size_t get_tasks_queued()`: Get the number of tasks currently waiting in the queue to be executed by the threads.
+ * `size_t get_tasks_running()`: Get the number of tasks currently being executed by the threads.
+ * `size_t get_tasks_total()`: Get the total number of unfinished tasks: either still waiting in the queue, or running in a thread. Note that `get_tasks_total() == get_tasks_queued() + get_tasks_running()`.
+ * `BS::concurrency_t get_thread_count()`: Get the number of threads in the pool.
+ * `std::vector get_thread_ids()`: Get a vector containing the unique identifiers for each of the pool's threads, as obtained by `std::thread::get_id()`.
+* Task submission without futures (`T` and `F` are template parameters):
+ * `void detach_task(F&& task)`: Submit a function with no arguments and no return value into the task queue. To push a function with arguments, enclose it in a lambda expression. Does not return a future, so the user must use `wait()` or some other method to ensure that the task finishes executing, otherwise bad things will happen.
+ * `void detach_blocks(T first_index, T index_after_last, F&& block, size_t num_blocks = 0)`: Parallelize a loop by automatically splitting it into blocks and submitting each block separately to the queue. The `block` function takes two arguments, the start and end of the block, so that it is only called only once per block, but it is up to the user make sure the block function correctly deals with all the indices in each block. Does not return a `BS::multi_future`, so the user must use `wait()` or some other method to ensure that the loop finishes executing, otherwise bad things will happen.
+ * `void detach_loop(T first_index, T index_after_last, F&& loop, size_t num_blocks = 0)`: Parallelize a loop by automatically splitting it into blocks and submitting each block separately to the queue. The `loop` function takes one argument, the loop index, so that it is called many times per block. Does not return a `BS::multi_future`, so the user must use `wait()` or some other method to ensure that the loop finishes executing, otherwise bad things will happen.
+ * `void detach_sequence(T first_index, T index_after_last, F&& sequence)`: Submit a sequence of tasks enumerated by indices to the queue. Does not return a `BS::multi_future`, so the user must use `wait()` or some other method to ensure that the sequence finishes executing, otherwise bad things will happen.
+* Task submission with futures (`T`, `F`, and `R` are template parameters):
+ * `std::future submit_task(F&& task)`: Submit a function with no arguments into the task queue. To submit a function with arguments, enclose it in a lambda expression. If the function has a return value, get a future for the eventual returned value. If the function has no return value, get an `std::future` which can be used to wait until the task finishes.
+ * `BS::multi_future submit_blocks(T first_index, T index_after_last, F&& block, size_t num_blocks = 0)`: Parallelize a loop by automatically splitting it into blocks and submitting each block separately to the queue. The `block` function takes two arguments, the start and end of the block, so that it is only called only once per block, but it is up to the user make sure the block function correctly deals with all the indices in each block. Returns a `BS::multi_future` that contains the futures for all of the blocks.
+ * `BS::multi_future submit_loop(T first_index, T index_after_last, F&& loop, size_t num_blocks = 0)`: Parallelize a loop by automatically splitting it into blocks and submitting each block separately to the queue. The `loop` function takes one argument, the loop index, so that it is called many times per block. It must have no return value. Returns a `BS::multi_future` that contains the futures for all of the blocks.
+ * `BS::multi_future submit_sequence(T first_index, T index_after_last, F&& sequence)`: Submit a sequence of tasks enumerated by indices to the queue. Returns a `BS::multi_future` that contains the futures for all of the tasks.
+* Task management:
+ * `void purge()`: Purge all the tasks waiting in the queue. Tasks that are currently running will not be affected, but any tasks still waiting in the queue will be discarded, and will never be executed by the threads. Please note that there is no way to restore the purged tasks.
+* Waiting for tasks (`R` and `P`, `C`, and `D` are template parameters):
+ * `void wait()`: Wait for tasks to be completed. Normally, this function waits for all tasks, both those that are currently running in the threads and those that are still waiting in the queue. However, if the pool is paused, this function only waits for the currently running tasks (otherwise it would wait forever). Note: To wait for just one specific task, use `submit_task()` instead, and call the `wait()` member function of the generated future.
+ * `bool wait_for(std::chrono::duration& duration)`: Wait for tasks to be completed, but stop waiting after the specified duration has passed. Returns `true` if all tasks finished running, `false` if the duration expired but some tasks are still running.
+ * `bool wait_until(std::chrono::time_point& timeout_time)`: Wait for tasks to be completed, but stop waiting after the specified time point has been reached. Returns `true` if all tasks finished running, `false` if the time point was reached but some tasks are still running.
+
+#### Optional features for the `BS::thread_pool` class
+
+The thread pool has several optional features that must be explicitly enabled using macros.
+
+* **Task priority:** Enabled by defining the macro `BS_THREAD_POOL_ENABLE_PRIORITY`.
+ * When enabled, the priority of a task or group of tasks may be specified as an additional argument (at the end of the argument list) to `detach_task()`, `submit_task()`, `detach_blocks()`, `submit_blocks()`, `detach_loop()`, `submit_loop()`, `detach_sequence()`, and `submit_sequence()`. If the priority is not specified, the default value will be 0.
+ * The priority is a number of type `BS::priority_t`, which is a signed 16-bit integer, so it can have any value between -32,768 and 32,767. The tasks will be executed in priority order from highest to lowest.
+ * The namespace `BS::pr` contains some pre-defined priorities: `BS::pr::highest`, `BS::pr::high`, `BS::pr::normal`, `BS::pr::low`, and `BS::pr::lowest`.
+* **Pausing:** Enabled by defining the macro `BS_THREAD_POOL_ENABLE_PAUSE`. Adds the following member functions:
+ * `void pause()`: Pause the pool. The workers will temporarily stop retrieving new tasks out of the queue, although any tasks already executed will keep running until they are finished.
+ * `void unpause()`: Unpause the pool. The workers will resume retrieving new tasks out of the queue.
+ * `bool is_paused()`: Check whether the pool is currently paused.
+* **Getting the native handles of the threads:** Enabled by defining the macro `BS_THREAD_POOL_ENABLE_NATIVE_HANDLES`. Adds the following member function:
+ * `std::vector get_native_handles()`: Get a vector containing the underlying implementation-defined thread handles for each of the pool's threads.
+* **Wait deadlock checks:** Enabled by defining the macro `BS_THREAD_POOL_ENABLE_WAIT_DEADLOCK_CHECK`.
+ * When enabled, `wait()`, `wait_for()`, and `wait_until()` will check whether the user tried to call them from within a thread of the same pool, which would result in a deadlock. If so, they will throw the exception `BS::thread_pool::wait_deadlock` instead of waiting.
+
+#### The `BS::this_thread` namespace
+
+The namespace `BS::this_thread` provides functionality similar to `std::this_thread`. It contains the following function objects:
+
+* `BS::this_thread::get_index()` can be used to get the index of the current thread. If this thread belongs to a `BS::thread_pool` object, it will have an index from 0 to `BS::thread_pool::get_thread_count() - 1`. Otherwise, for example if this thread is the main thread or an independent [`std::thread`](https://en.cppreference.com/w/cpp/thread/thread), [`std::nullopt`](https://en.cppreference.com/w/cpp/utility/optional/nullopt) will be returned.
+* `BS::this_thread::get_pool()` can be used to get the pointer to the thread pool that owns the current thread. If this thread belongs to a `BS::thread_pool` object, a pointer to that object will be returned. Otherwise, `std::nullopt` will be returned.
+* In both cases, an [`std::optional`](https://en.cppreference.com/w/cpp/utility/optional) object will be returned, of type `BS::this_thread::optional_index` or `BS::this_thread::optional_pool` respectively. Unless you are 100% sure this thread is in a pool, first use [`std::optional::has_value()`](https://en.cppreference.com/w/cpp/utility/optional/operator_bool) to check if it contains a value, and if so, use [`std::optional::value()`](https://en.cppreference.com/w/cpp/utility/optional/value) to obtain that value.
+
+#### The `BS::multi_future` class
+
+`BS::multi_future` is a helper class used to facilitate waiting for and/or getting the results of multiple futures at once. It is defined as a specialization of `std::vector>`. This means that all of the member functions that can be used on an [`std::vector`](https://en.cppreference.com/w/cpp/container/vector) can also be used on a `BS::multi_future`. For example, you may use a range-based for loop with a `BS::multi_future`, since it has iterators.
+
+In addition to inherited member functions, `BS::multi_future` has the following specialized member functions (`R` and `P`, `C`, and `D` are template parameters):
+
+* `[void or std::vector] get()`: Get the results from all the futures stored in this `BS::multi_future`, rethrowing any stored exceptions. If the futures return `void`, this function returns `void` as well. If the futures return a type `T`, this function returns a vector containing the results.
+* `size_t ready_count()`: Check how many of the futures stored in this `BS::multi_future` are ready.
+* `bool valid()`: Check if all the futures stored in this `BS::multi_future` are valid.
+* `void wait()`: Wait for all the futures stored in this `BS::multi_future`.
+* `bool wait_for(std::chrono::duration& duration)`: Wait for all the futures stored in this `BS::multi_future`, but stop waiting after the specified duration has passed. Returns `true` if all futures have been waited for before the duration expired, `false` otherwise.
+* `bool wait_until(std::chrono::time_point& timeout_time)`: Wait for all the futures stored in this `multi_future` object, but stop waiting after the specified time point has been reached. Returns `true` if all futures have been waited for before the time point was reached, `false` otherwise.
+
+### Utility header file (`BS_thread_pool_utils.hpp`)
+
+#### The `BS::signaller` class
+
+`BS::signaller` is a utility class which can be used to allow simple signalling between threads. This class is really just a convenient wrapper around [`std::promise`](https://en.cppreference.com/w/cpp/thread/promise), which contains both the promise and its future. It has the following member functions:
+
+* `signaller()`: Construct a new signaller.
+* `void wait()`: Wait until the signaller is ready.
+* `void ready()`: Inform any waiting threads that the signaller is ready.
+
+#### The `BS::synced_stream` class
+
+`BS::synced_stream` is a utility class which can be used to synchronize printing to an output stream by different threads. It has the following member functions (`T` is a template parameter pack):
+
+* `synced_stream(std::ostream& stream = std::cout)`: Construct a new synced stream which prints to the given output stream.
+* `void print(T&&... items)`: Print any number of items into the output stream. Ensures that no other threads print to this stream simultaneously, as long as they all exclusively use the same `synced_stream` object to print.
+* `void println(T&&... items)`: Print any number of items into the output stream, followed by a newline character.
+
+In addition, the class comes with two stream manipulators, which are meant to help the compiler figure out which template specializations to use with the class:
+
+* `BS::synced_stream::endl`: An explicit cast of `std::endl`. Prints a newline character to the stream, and then flushes it. Should only be used if flushing is desired, otherwise a newline character should be used instead.
+* `BS::synced_stream::flush`: An explicit cast of `std::flush`. Used to flush the stream.
+
+#### The `BS::timer` class
+
+`BS::timer` is a utility class which can be used to measure execution time for benchmarking purposes. It has the following member functions:
+
+* `timer()`: Construct a new timer and immediately start measuring time.
+* `void start()`: Start (or restart) measuring time. Note that the timer starts ticking as soon as the object is created, so this is only necessary if we want to restart the clock later.
+* `void stop()`: Stop measuring time and store the elapsed time since the object was constructed or since `start()` was last called.
+* `std::chrono::milliseconds::rep current_ms()`: Get the number of milliseconds that have elapsed since the object was constructed or since `start()` was last called, but keep the timer ticking.
+* `std::chrono::milliseconds::rep ms()`: Get the number of milliseconds stored when `stop()` was last called.
## About the project
### Issue and pull request policy
-This package is under continuous and active development. If you encounter any bugs, or if you would like to request any additional features, please feel free to [open a new issue on GitHub](https://github.com/bshoshany/thread-pool/issues) and I will look into it as soon as I can.
+This library is under continuous and active development. If you encounter any bugs, or if you would like to request any additional features, please feel free to [open a new issue on GitHub](https://github.com/bshoshany/thread-pool/issues) and I will look into it as soon as I can.
Contributions are always welcome. However, I release my projects in cumulative updates after editing and testing them locally on my system, so my policy is not to accept any pull requests. If you open a pull request, and I decide to incorporate your suggestion into the project, I will first modify your code to comply with the project's coding conventions (formatting, syntax, naming, comments, programming practices, etc.), and perform some tests to ensure that the change doesn't break anything. I will then merge it into the next release of the project, possibly together with some other changes. The new release will also include a note in `CHANGELOG.md` with a link to your pull request, and modifications to the documentation in `README.md` as needed.
@@ -1346,7 +2007,7 @@ You can use the following BibTeX entry:
}
```
-Please note that the [companion paper on arXiv](https://arxiv.org/abs/2105.00613) is updated infrequently. The paper is intended to facilitate discovery of the package by scientists who may find it useful for scientific computing purposes and to allow citing the package in scientific research, but most users should read the `README.md` file on [the GitHub repository](https://github.com/bshoshany/thread-pool) instead, as it is guaranteed to always be up to date.
+Please note that the [companion paper on arXiv](https://arxiv.org/abs/2105.00613) is updated infrequently. The paper is intended to facilitate discovery of the library by scientists who may find it useful for scientific computing purposes and to allow citing the library in scientific research, but most users should read the `README.md` file on [the GitHub repository](https://github.com/bshoshany/thread-pool) instead, as it is guaranteed to always be up to date.
### Learning more about C++
diff --git a/include/BS_thread_pool.hpp b/include/BS_thread_pool.hpp
index cb6fe08..ebee9f1 100644
--- a/include/BS_thread_pool.hpp
+++ b/include/BS_thread_pool.hpp
@@ -3,38 +3,148 @@
/**
* @file BS_thread_pool.hpp
* @author Barak Shoshany (baraksh@gmail.com) (http://baraksh.com)
- * @version 3.5.0
- * @date 2023-05-25
+ * @version 4.0.0
+ * @date 2023-12-27
* @copyright Copyright (c) 2023 Barak Shoshany. Licensed under the MIT license. If you found this project useful, please consider starring it on GitHub! If you use this library in software of any kind, please provide a link to the GitHub repository https://github.com/bshoshany/thread-pool in the source code and documentation. If you use this library in published research, please cite it as follows: Barak Shoshany, "A C++17 Thread Pool for High-Performance Scientific Computing", doi:10.5281/zenodo.4742687, arXiv:2105.00613 (May 2021)
*
- * @brief BS::thread_pool: a fast, lightweight, and easy-to-use C++17 thread pool library. This header file contains the entire library, including the main BS::thread_pool class and the helper classes BS::multi_future, BS::blocks, BS:synced_stream, and BS::timer.
+ * @brief BS::thread_pool: a fast, lightweight, and easy-to-use C++17 thread pool library. This header file contains the main thread pool class and some additional classes and definitions. No other files are needed in order to use the thread pool itself.
*/
-#define BS_THREAD_POOL_VERSION "v3.5.0 (2023-05-25)"
-
#include // std::chrono
#include // std::condition_variable
+#include // std::size_t
+#include // std::int_least16_t
#include // std::current_exception
-#include // std::bind, std::function, std::invoke
-#include // std::future, std::promise
-#include // std::cout, std::endl, std::flush, std::ostream
+#include // std::function
+#include // std::future, std::future_status, std::promise
#include // std::make_shared, std::make_unique, std::shared_ptr, std::unique_ptr
#include // std::mutex, std::scoped_lock, std::unique_lock
-#include // std::queue
+#include // std::nullopt, std::optional
+#include // std::priority_queue, std::queue
+#include // std::runtime_error
#include // std::thread
-#include // std::common_type_t, std::conditional_t, std::decay_t, std::invoke_result_t, std::is_void_v
-#include // std::forward, std::move, std::swap
+#include // std::conditional_t, std::decay_t, std::invoke_result_t, std::is_void_v, std::remove_const_t
+#include // std::forward, std::move
#include // std::vector
-namespace BS
-{
/**
- * @brief A convenient shorthand for the type of std::thread::hardware_concurrency(). Should evaluate to unsigned int.
+ * @brief A namespace used by Barak Shoshany's projects.
+ */
+namespace BS {
+// A macro containing the version of the thread pool library.
+#define BS_THREAD_POOL_VERSION "v4.0.0 (2023-12-27)"
+
+class thread_pool;
+
+/**
+ * @brief A type to represent the size of things.
+ */
+using size_t = std::size_t;
+
+/**
+ * @brief A convenient shorthand for the type of `std::thread::hardware_concurrency()`. Should evaluate to unsigned int.
*/
using concurrency_t = std::invoke_result_t;
-// ============================================================================================= //
-// Begin class multi_future //
+#ifdef BS_THREAD_POOL_ENABLE_PRIORITY
+/**
+ * @brief A type used to indicate the priority of a task. Defined to be an integer with a width of (at least) 16 bits.
+ */
+using priority_t = std::int_least16_t;
+
+/**
+ * @brief A namespace containing some pre-defined priorities for convenience.
+ */
+namespace pr {
+ constexpr priority_t highest = 32767;
+ constexpr priority_t high = 16383;
+ constexpr priority_t normal = 0;
+ constexpr priority_t low = -16384;
+ constexpr priority_t lowest = -32768;
+} // namespace pr
+
+// Macros used internally to enable or disable the priority arguments in the relevant functions.
+#define BS_THREAD_POOL_PRIORITY_INPUT , const priority_t priority = 0
+#define BS_THREAD_POOL_PRIORITY_OUTPUT , priority
+#else
+#define BS_THREAD_POOL_PRIORITY_INPUT
+#define BS_THREAD_POOL_PRIORITY_OUTPUT
+#endif
+
+/**
+ * @brief A namespace used to obtain information about the current thread.
+ */
+namespace this_thread {
+ /**
+ * @brief A type returned by `BS::this_thread::get_index()` which can optionally contain the index of a thread, if that thread belongs to a `BS::thread_pool`. Otherwise, it will contain no value.
+ */
+ using optional_index = std::optional;
+
+ /**
+ * @brief A type returned by `BS::this_thread::get_pool()` which can optionally contain the pointer to the pool that owns a thread, if that thread belongs to a `BS::thread_pool`. Otherwise, it will contain no value.
+ */
+ using optional_pool = std::optional;
+
+ /**
+ * @brief A helper class to store information about the index of the current thread.
+ */
+ class [[nodiscard]] thread_info_index
+ {
+ friend class BS::thread_pool;
+
+ public:
+ /**
+ * @brief Get the index of the current thread. If this thread belongs to a `BS::thread_pool` object, it will have an index from 0 to `BS::thread_pool::get_thread_count() - 1`. Otherwise, for example if this thread is the main thread or an independent `std::thread`, `std::nullopt` will be returned.
+ *
+ * @return An `std::optional` object, optionally containing a thread index. Unless you are 100% sure this thread is in a pool, first use `std::optional::has_value()` to check if it contains a value, and if so, use `std::optional::value()` to obtain that value.
+ */
+ [[nodiscard]] optional_index operator()() const
+ {
+ return index;
+ }
+
+ private:
+ /**
+ * @brief The index of the current thread.
+ */
+ optional_index index = std::nullopt;
+ }; // class thread_info_index
+
+ /**
+ * @brief A helper class to store information about the thread pool that owns the current thread.
+ */
+ class [[nodiscard]] thread_info_pool
+ {
+ friend class BS::thread_pool;
+
+ public:
+ /**
+ * @brief Get the pointer to the thread pool that owns the current thread. If this thread belongs to a `BS::thread_pool` object, a pointer to that object will be returned. Otherwise, for example if this thread is the main thread or an independent `std::thread`, `std::nullopt` will be returned.
+ *
+ * @return An `std::optional` object, optionally containing a pointer to a thread pool. Unless you are 100% sure this thread is in a pool, first use `std::optional::has_value()` to check if it contains a value, and if so, use `std::optional::value()` to obtain that value.
+ */
+ [[nodiscard]] optional_pool operator()() const
+ {
+ return pool;
+ }
+
+ private:
+ /**
+ * @brief A pointer to the thread pool that owns the current thread.
+ */
+ optional_pool pool = std::nullopt;
+ }; // class thread_info_pool
+
+ /**
+ * @brief A `thread_local` object used to obtain information about the index of the current thread.
+ */
+ thread_local thread_info_index get_index;
+
+ /**
+ * @brief A `thread_local` object used to obtain information about the thread pool that owns the current thread.
+ */
+ thread_local thread_info_pool get_pool;
+} // namespace this_thread
/**
* @brief A helper class to facilitate waiting for and/or getting the results of multiple futures at once.
@@ -42,196 +152,122 @@ using concurrency_t = std::invoke_result_t
-class [[nodiscard]] multi_future
+class [[nodiscard]] multi_future : public std::vector>
{
public:
- /**
- * @brief Construct a multi_future object with the given number of futures.
- *
- * @param num_futures_ The desired number of futures to store.
- */
- multi_future(const size_t num_futures_ = 0) : futures(num_futures_) {}
+ // Inherit all constructors from the base class `std::vector`.
+ using std::vector>::vector;
+
+ // The copy constructor and copy assignment operator are deleted. The elements stored in a `multi_future` are futures, which cannot be copied.
+ multi_future(const multi_future&) = delete;
+ multi_future& operator=(const multi_future&) = delete;
+
+ // The move constructor and move assignment operator are defaulted.
+ multi_future(multi_future&&) = default;
+ multi_future& operator=(multi_future&&) = default;
/**
- * @brief Get the results from all the futures stored in this multi_future object, rethrowing any stored exceptions.
+ * @brief Get the results from all the futures stored in this `multi_future`, rethrowing any stored exceptions.
*
- * @return If the futures return void, this function returns void as well. Otherwise, it returns a vector containing the results.
+ * @return If the futures return `void`, this function returns `void` as well. Otherwise, it returns a vector containing the results.
*/
[[nodiscard]] std::conditional_t, void, std::vector> get()
{
if constexpr (std::is_void_v)
{
- for (size_t i = 0; i < futures.size(); ++i)
- futures[i].get();
+ for (std::future& future : *this)
+ future.get();
return;
}
else
{
- std::vector results(futures.size());
- for (size_t i = 0; i < futures.size(); ++i)
- results[i] = futures[i].get();
+ std::vector results;
+ results.reserve(this->size());
+ for (std::future& future : *this)
+ results.push_back(future.get());
return results;
}
}
/**
- * @brief Get a reference to one of the futures stored in this multi_future object.
+ * @brief Check how many of the futures stored in this `multi_future` are ready.
*
- * @param i The index of the desired future.
- * @return The future.
+ * @return The number of ready futures.
*/
- [[nodiscard]] std::future& operator[](const size_t i)
+ [[nodiscard]] size_t ready_count() const
{
- return futures[i];
+ size_t count = 0;
+ for (const std::future& future : *this)
+ {
+ if (future.wait_for(std::chrono::duration::zero()) == std::future_status::ready)
+ ++count;
+ }
+ return count;
}
/**
- * @brief Append a future to this multi_future object.
+ * @brief Check if all the futures stored in this `multi_future` are valid.
*
- * @param future The future to append.
+ * @return `true` if all futures are valid, `false` if at least one of the futures is not valid.
*/
- void push_back(std::future future)
+ [[nodiscard]] bool valid() const
{
- futures.push_back(std::move(future));
+ bool is_valid = true;
+ for (const std::future& future : *this)
+ is_valid = is_valid && future.valid();
+ return is_valid;
}
/**
- * @brief Get the number of futures stored in this multi_future object.
- *
- * @return The number of futures.
- */
- [[nodiscard]] size_t size() const
- {
- return futures.size();
- }
-
- /**
- * @brief Wait for all the futures stored in this multi_future object.
+ * @brief Wait for all the futures stored in this `multi_future`.
*/
void wait() const
{
- for (size_t i = 0; i < futures.size(); ++i)
- futures[i].wait();
+ for (const std::future& future : *this)
+ future.wait();
}
-private:
/**
- * @brief A vector to store the futures.
- */
- std::vector> futures;
-};
-
-// End class multi_future //
-// ============================================================================================= //
-
-// ============================================================================================= //
-// Begin class blocks //
-
-/**
- * @brief A helper class to divide a range into blocks. Used by parallelize_loop() and push_loop().
- *
- * @tparam T1 The type of the first index in the range. Should be a signed or unsigned integer.
- * @tparam T2 The type of the index after the last index in the range. Should be a signed or unsigned integer. If T1 is not the same as T2, a common type will be automatically inferred.
- * @tparam T The common type of T1 and T2.
- */
-template >
-class [[nodiscard]] blocks
-{
-public:
- /**
- * @brief Construct a blocks object with the given specifications.
+ * @brief Wait for all the futures stored in this `multi_future`, but stop waiting after the specified duration has passed. This function first waits for the first future for the desired duration. If that future is ready before the duration expires, this function waits for the second future for whatever remains of the duration. It continues similarly until the duration expires.
*
- * @param first_index_ The first index in the range.
- * @param index_after_last_ The index after the last index in the range.
- * @param num_blocks_ The desired number of blocks to divide the range into.
+ * @tparam R An arithmetic type representing the number of ticks to wait.
+ * @tparam P An `std::ratio` representing the length of each tick in seconds.
+ * @param duration The amount of time to wait.
+ * @return `true` if all futures have been waited for before the duration expired, `false` otherwise.
*/
- blocks(const T1 first_index_, const T2 index_after_last_, const size_t num_blocks_) : first_index(static_cast(first_index_)), index_after_last(static_cast(index_after_last_)), num_blocks(num_blocks_)
+ template
+ bool wait_for(const std::chrono::duration& duration) const
{
- if (index_after_last < first_index)
- std::swap(index_after_last, first_index);
- total_size = static_cast(index_after_last - first_index);
- block_size = static_cast(total_size / num_blocks);
- if (block_size == 0)
+ const std::chrono::time_point start_time = std::chrono::steady_clock::now();
+ for (const std::future& future : *this)
{
- block_size = 1;
- num_blocks = (total_size > 1) ? total_size : 1;
+ future.wait_for(duration - (std::chrono::steady_clock::now() - start_time));
+ if (duration < std::chrono::steady_clock::now() - start_time)
+ return false;
}
+ return true;
}
/**
- * @brief Get the first index of a block.
+ * @brief Wait for all the futures stored in this `multi_future`, but stop waiting after the specified time point has been reached. This function first waits for the first future until the desired time point. If that future is ready before the time point is reached, this function waits for the second future until the desired time point. It continues similarly until the time point is reached.
*
- * @param i The block number.
- * @return The first index.
+ * @tparam C The type of the clock used to measure time.
+ * @tparam D An `std::chrono::duration` type used to indicate the time point.
+ * @param timeout_time The time point at which to stop waiting.
+ * @return `true` if all futures have been waited for before the time point was reached, `false` otherwise.
*/
- [[nodiscard]] T start(const size_t i) const
+ template
+ bool wait_until(const std::chrono::time_point& timeout_time) const
{
- return static_cast(i * block_size) + first_index;
+ for (const std::future& future : *this)
+ {
+ future.wait_until(timeout_time);
+ if (timeout_time < std::chrono::steady_clock::now())
+ return false;
+ }
+ return true;
}
-
- /**
- * @brief Get the index after the last index of a block.
- *
- * @param i The block number.
- * @return The index after the last index.
- */
- [[nodiscard]] T end(const size_t i) const
- {
- return (i == num_blocks - 1) ? index_after_last : (static_cast((i + 1) * block_size) + first_index);
- }
-
- /**
- * @brief Get the number of blocks. Note that this may be different than the desired number of blocks that was passed to the constructor.
- *
- * @return The number of blocks.
- */
- [[nodiscard]] size_t get_num_blocks() const
- {
- return num_blocks;
- }
-
- /**
- * @brief Get the total number of indices in the range.
- *
- * @return The total number of indices.
- */
- [[nodiscard]] size_t get_total_size() const
- {
- return total_size;
- }
-
-private:
- /**
- * @brief The size of each block (except possibly the last block).
- */
- size_t block_size = 0;
-
- /**
- * @brief The first index in the range.
- */
- T first_index = 0;
-
- /**
- * @brief The index after the last index in the range.
- */
- T index_after_last = 0;
-
- /**
- * @brief The number of blocks.
- */
- size_t num_blocks = 0;
-
- /**
- * @brief The total number of indices in the range.
- */
- size_t total_size = 0;
-};
-
-// End class blocks //
-// ============================================================================================= //
-
-// ============================================================================================= //
-// Begin class thread_pool //
+}; // class multi_future
/**
* @brief A fast, lightweight, and easy-to-use C++17 thread pool class.
@@ -244,21 +280,47 @@ public:
// ============================
/**
- * @brief Construct a new thread pool.
- *
- * @param thread_count_ The number of threads to use. The default value is the total number of hardware threads available, as reported by the implementation. This is usually determined by the number of cores in the CPU. If a core is hyperthreaded, it will count as two threads.
+ * @brief Construct a new thread pool. The number of threads will be the total number of hardware threads available, as reported by the implementation. This is usually determined by the number of cores in the CPU. If a core is hyperthreaded, it will count as two threads.
*/
- thread_pool(const concurrency_t thread_count_ = 0) : thread_count(determine_thread_count(thread_count_)), threads(std::make_unique(determine_thread_count(thread_count_)))
+ thread_pool() : thread_pool(0, [] {}) {}
+
+ /**
+ * @brief Construct a new thread pool with the specified number of threads.
+ *
+ * @param num_threads The number of threads to use.
+ */
+ explicit thread_pool(const concurrency_t num_threads) : thread_pool(num_threads, [] {}) {}
+
+ /**
+ * @brief Construct a new thread pool with the specified initialization function.
+ *
+ * @param init_task An initialization function to run in each thread before it starts to execute any submitted tasks. The function must take no arguments and have no return value. It will only be executed exactly once, when the thread is first constructed.
+ */
+ explicit thread_pool(const std::function& init_task) : thread_pool(0, init_task) {}
+
+ /**
+ * @brief Construct a new thread pool with the specified number of threads and initialization function.
+ *
+ * @param num_threads The number of threads to use.
+ * @param init_task An initialization function to run in each thread before it starts to execute any submitted tasks. The function must take no arguments and have no return value. It will only be executed exactly once, when the thread is first constructed.
+ */
+ thread_pool(const concurrency_t num_threads, const std::function& init_task) : thread_count(determine_thread_count(num_threads)), threads(std::make_unique(determine_thread_count(num_threads)))
{
- create_threads();
+ create_threads(init_task);
}
+ // The copy and move constructors and assignment operators are deleted. The thread pool uses a mutex, which cannot be copied or moved.
+ thread_pool(const thread_pool&) = delete;
+ thread_pool(thread_pool&&) = delete;
+ thread_pool& operator=(const thread_pool&) = delete;
+ thread_pool& operator=(thread_pool&&) = delete;
+
/**
* @brief Destruct the thread pool. Waits for all tasks to complete, then destroys all threads. Note that if the pool is paused, then any tasks still in the queue will never be executed.
*/
~thread_pool()
{
- wait_for_tasks();
+ wait();
destroy_threads();
}
@@ -266,6 +328,23 @@ public:
// Public member functions
// =======================
+#ifdef BS_THREAD_POOL_ENABLE_NATIVE_HANDLES
+ /**
+ * @brief Get a vector containing the underlying implementation-defined thread handles for each of the pool's threads, as obtained by `std::thread::native_handle()`. Only enabled if `BS_THREAD_POOL_ENABLE_NATIVE_HANDLES` is defined.
+ *
+ * @return The native thread handles.
+ */
+ [[nodiscard]] std::vector get_native_handles() const
+ {
+ std::vector native_handles(thread_count);
+ for (concurrency_t i = 0; i < thread_count; ++i)
+ {
+ native_handles[i] = threads[i].native_handle();
+ }
+ return native_handles;
+ }
+#endif
+
/**
* @brief Get the number of tasks currently waiting in the queue to be executed by the threads.
*
@@ -289,7 +368,7 @@ public:
}
/**
- * @brief Get the total number of unfinished tasks: either still waiting in the queue, or running in a thread. Note that get_tasks_total() == get_tasks_queued() + get_tasks_running().
+ * @brief Get the total number of unfinished tasks: either still waiting in the queue, or running in a thread. Note that `get_tasks_total() == get_tasks_queued() + get_tasks_running()`.
*
* @return The total number of tasks.
*/
@@ -310,9 +389,25 @@ public:
}
/**
- * @brief Check whether the pool is currently paused.
+ * @brief Get a vector containing the unique identifiers for each of the pool's threads, as obtained by `std::thread::get_id()`.
*
- * @return true if the pool is paused, false if it is not paused.
+ * @return The unique thread identifiers.
+ */
+ [[nodiscard]] std::vector get_thread_ids() const
+ {
+ std::vector thread_ids(thread_count);
+ for (concurrency_t i = 0; i < thread_count; ++i)
+ {
+ thread_ids[i] = threads[i].get_id();
+ }
+ return thread_ids;
+ }
+
+#ifdef BS_THREAD_POOL_ENABLE_PAUSE
+ /**
+ * @brief Check whether the pool is currently paused. Only enabled if `BS_THREAD_POOL_ENABLE_PAUSE` is defined.
+ *
+ * @return `true` if the pool is paused, `false` if it is not paused.
*/
[[nodiscard]] bool is_paused() const
{
@@ -321,61 +416,14 @@ public:
}
/**
- * @brief Parallelize a loop by automatically splitting it into blocks and submitting each block separately to the queue. Returns a multi_future object that contains the futures for all of the blocks.
- *
- * @tparam F The type of the function to loop through.
- * @tparam T1 The type of the first index in the loop. Should be a signed or unsigned integer.
- * @tparam T2 The type of the index after the last index in the loop. Should be a signed or unsigned integer. If T1 is not the same as T2, a common type will be automatically inferred.
- * @tparam T The common type of T1 and T2.
- * @tparam R The return value of the loop function F (can be void).
- * @param first_index The first index in the loop.
- * @param index_after_last The index after the last index in the loop. The loop will iterate from first_index to (index_after_last - 1) inclusive. In other words, it will be equivalent to "for (T i = first_index; i < index_after_last; ++i)". Note that if index_after_last == first_index, no blocks will be submitted.
- * @param loop The function to loop through. Will be called once per block. Should take exactly two arguments: the first index in the block and the index after the last index in the block. loop(start, end) should typically involve a loop of the form "for (T i = start; i < end; ++i)".
- * @param num_blocks The maximum number of blocks to split the loop into. The default is to use the number of threads in the pool.
- * @return A multi_future object that can be used to wait for all the blocks to finish. If the loop function returns a value, the multi_future object can also be used to obtain the values returned by each block.
- */
- template , typename R = std::invoke_result_t, T, T>>
- [[nodiscard]] multi_future parallelize_loop(const T1 first_index, const T2 index_after_last, F&& loop, const size_t num_blocks = 0)
- {
- blocks blks(first_index, index_after_last, num_blocks ? num_blocks : thread_count);
- if (blks.get_total_size() > 0)
- {
- multi_future mf(blks.get_num_blocks());
- for (size_t i = 0; i < blks.get_num_blocks(); ++i)
- mf[i] = submit(std::forward(loop), blks.start(i), blks.end(i));
- return mf;
- }
- else
- {
- return multi_future();
- }
- }
-
- /**
- * @brief Parallelize a loop by automatically splitting it into blocks and submitting each block separately to the queue. Returns a multi_future object that contains the futures for all of the blocks. This overload is used for the special case where the first index is 0.
- *
- * @tparam F The type of the function to loop through.
- * @tparam T The type of the loop indices. Should be a signed or unsigned integer.
- * @tparam R The return value of the loop function F (can be void).
- * @param index_after_last The index after the last index in the loop. The loop will iterate from 0 to (index_after_last - 1) inclusive. In other words, it will be equivalent to "for (T i = 0; i < index_after_last; ++i)". Note that if index_after_last == 0, no blocks will be submitted.
- * @param loop The function to loop through. Will be called once per block. Should take exactly two arguments: the first index in the block and the index after the last index in the block. loop(start, end) should typically involve a loop of the form "for (T i = start; i < end; ++i)".
- * @param num_blocks The maximum number of blocks to split the loop into. The default is to use the number of threads in the pool.
- * @return A multi_future object that can be used to wait for all the blocks to finish. If the loop function returns a value, the multi_future object can also be used to obtain the values returned by each block.
- */
- template , T, T>>
- [[nodiscard]] multi_future parallelize_loop(const T index_after_last, F&& loop, const size_t num_blocks = 0)
- {
- return parallelize_loop(0, index_after_last, std::forward(loop), num_blocks);
- }
-
- /**
- * @brief Pause the pool. The workers will temporarily stop retrieving new tasks out of the queue, although any tasks already executed will keep running until they are finished.
+ * @brief Pause the pool. The workers will temporarily stop retrieving new tasks out of the queue, although any tasks already executed will keep running until they are finished. Only enabled if `BS_THREAD_POOL_ENABLE_PAUSE` is defined.
*/
void pause()
{
const std::scoped_lock tasks_lock(tasks_mutex);
paused = true;
}
+#endif
/**
* @brief Purge all the tasks waiting in the queue. Tasks that are currently running will not be affected, but any tasks still waiting in the queue will be discarded, and will never be executed by the threads. Please note that there is no way to restore the purged tasks.
@@ -388,107 +436,175 @@ public:
}
/**
- * @brief Parallelize a loop by automatically splitting it into blocks and submitting each block separately to the queue. Does not return a multi_future, so the user must use wait_for_tasks() or some other method to ensure that the loop finishes executing, otherwise bad things will happen.
- *
- * @tparam F The type of the function to loop through.
- * @tparam T1 The type of the first index in the loop. Should be a signed or unsigned integer.
- * @tparam T2 The type of the index after the last index in the loop. Should be a signed or unsigned integer. If T1 is not the same as T2, a common type will be automatically inferred.
- * @tparam T The common type of T1 and T2.
- * @param first_index The first index in the loop.
- * @param index_after_last The index after the last index in the loop. The loop will iterate from first_index to (index_after_last - 1) inclusive. In other words, it will be equivalent to "for (T i = first_index; i < index_after_last; ++i)". Note that if index_after_last == first_index, no blocks will be submitted.
- * @param loop The function to loop through. Will be called once per block. Should take exactly two arguments: the first index in the block and the index after the last index in the block. loop(start, end) should typically involve a loop of the form "for (T i = start; i < end; ++i)".
- * @param num_blocks The maximum number of blocks to split the loop into. The default is to use the number of threads in the pool.
- */
- template >
- void push_loop(const T1 first_index, const T2 index_after_last, F&& loop, const size_t num_blocks = 0)
- {
- blocks blks(first_index, index_after_last, num_blocks ? num_blocks : thread_count);
- if (blks.get_total_size() > 0)
- {
- for (size_t i = 0; i < blks.get_num_blocks(); ++i)
- push_task(std::forward(loop), blks.start(i), blks.end(i));
- }
- }
-
- /**
- * @brief Parallelize a loop by automatically splitting it into blocks and submitting each block separately to the queue. Does not return a multi_future, so the user must use wait_for_tasks() or some other method to ensure that the loop finishes executing, otherwise bad things will happen. This overload is used for the special case where the first index is 0.
- *
- * @tparam F The type of the function to loop through.
- * @tparam T The type of the loop indices. Should be a signed or unsigned integer.
- * @param index_after_last The index after the last index in the loop. The loop will iterate from 0 to (index_after_last - 1) inclusive. In other words, it will be equivalent to "for (T i = 0; i < index_after_last; ++i)". Note that if index_after_last == 0, no blocks will be submitted.
- * @param loop The function to loop through. Will be called once per block. Should take exactly two arguments: the first index in the block and the index after the last index in the block. loop(start, end) should typically involve a loop of the form "for (T i = start; i < end; ++i)".
- * @param num_blocks The maximum number of blocks to split the loop into. The default is to use the number of threads in the pool.
- */
- template
- void push_loop(const T index_after_last, F&& loop, const size_t num_blocks = 0)
- {
- push_loop(0, index_after_last, std::forward(loop), num_blocks);
- }
-
- /**
- * @brief Push a function with zero or more arguments, but no return value, into the task queue. Does not return a future, so the user must use wait_for_tasks() or some other method to ensure that the task finishes executing, otherwise bad things will happen.
+ * @brief Submit a function with no arguments and no return value into the task queue, with the specified priority. To push a function with arguments, enclose it in a lambda expression. Does not return a future, so the user must use `wait()` or some other method to ensure that the task finishes executing, otherwise bad things will happen.
*
* @tparam F The type of the function.
- * @tparam A The types of the arguments.
* @param task The function to push.
- * @param args The zero or more arguments to pass to the function. Note that if the task is a class member function, the first argument must be a pointer to the object, i.e. &object (or this), followed by the actual arguments.
+ * @param priority The priority of the task. Should be between -32,768 and 32,767 (a signed 16-bit integer). The default is 0. Only enabled if `BS_THREAD_POOL_ENABLE_PRIORITY` is defined.
*/
- template
- void push_task(F&& task, A&&... args)
+ template
+ void detach_task(F&& task BS_THREAD_POOL_PRIORITY_INPUT)
{
{
const std::scoped_lock tasks_lock(tasks_mutex);
- tasks.push(std::bind(std::forward(task), std::forward(args)...)); // cppcheck-suppress ignoredReturnValue
+ tasks.emplace(std::forward(task) BS_THREAD_POOL_PRIORITY_OUTPUT);
}
task_available_cv.notify_one();
}
/**
- * @brief Reset the number of threads in the pool. Waits for all currently running tasks to be completed, then destroys all threads in the pool and creates a new thread pool with the new number of threads. Any tasks that were waiting in the queue before the pool was reset will then be executed by the new threads. If the pool was paused before resetting it, the new pool will be paused as well.
+ * @brief Parallelize a loop by automatically splitting it into blocks and submitting each block separately to the queue, with the specified priority. The block function takes two arguments, the start and end of the block, so that it is only called only once per block, but it is up to the user make sure the block function correctly deals with all the indices in each block. Does not return a `multi_future`, so the user must use `wait()` or some other method to ensure that the loop finishes executing, otherwise bad things will happen.
*
- * @param thread_count_ The number of threads to use. The default value is the total number of hardware threads available, as reported by the implementation. This is usually determined by the number of cores in the CPU. If a core is hyperthreaded, it will count as two threads.
+ * @tparam T The type of the indices. 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.
+ * @param index_after_last The index after the last index in the loop. The loop will iterate from `first_index` to `(index_after_last - 1)` inclusive. In other words, it will be equivalent to `for (T i = first_index; i < index_after_last; ++i)`. Note that if `index_after_last <= first_index`, no blocks will be submitted.
+ * @param block A function that will be called once per block. Should take exactly two arguments: the first index in the block and the index after the last index in the block. `block(start, end)` should typically involve a loop of the form `for (T i = start; i < end; ++i)`.
+ * @param num_blocks The maximum number of blocks to split the loop into. The default is 0, which means the number of blocks will be equal to the number of threads in the pool.
+ * @param priority The priority of the tasks. Should be between -32,768 and 32,767 (a signed 16-bit integer). The default is 0. Only enabled if `BS_THREAD_POOL_ENABLE_PRIORITY` is defined.
*/
- void reset(const concurrency_t thread_count_ = 0)
+ template
+ void detach_blocks(const T first_index, const T index_after_last, F&& block, const size_t num_blocks = 0 BS_THREAD_POOL_PRIORITY_INPUT)
{
- std::unique_lock tasks_lock(tasks_mutex);
- const bool was_paused = paused;
- paused = true;
- tasks_lock.unlock();
- wait_for_tasks();
- destroy_threads();
- thread_count = determine_thread_count(thread_count_);
- threads = std::make_unique(thread_count);
- paused = was_paused;
- create_threads();
+ if (index_after_last > first_index)
+ {
+ const blocks blks(first_index, index_after_last, num_blocks ? num_blocks : thread_count);
+ for (size_t blk = 0; blk < blks.get_num_blocks(); ++blk)
+ detach_task(
+ [block = std::forward(block), start = blks.start(blk), end = blks.end(blk)]
+ {
+ block(start, end);
+ } BS_THREAD_POOL_PRIORITY_OUTPUT);
+ }
}
/**
- * @brief Submit a function with zero or more arguments into the task queue. If the function has a return value, get a future for the eventual returned value. If the function has no return value, get an std::future which can be used to wait until the task finishes.
+ * @brief Parallelize a loop by automatically splitting it into blocks and submitting each block separately to the queue, with the specified priority. The loop function takes one argument, the loop index, so that it is called many times per block. Does not return a `multi_future`, so the user must use `wait()` or some other method to ensure that the loop finishes executing, otherwise bad things will happen.
+ *
+ * @tparam T The type of the indices. 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.
+ * @param index_after_last The index after the last index in the loop. The loop will iterate from `first_index` to `(index_after_last - 1)` inclusive. In other words, it will be equivalent to `for (T i = first_index; i < index_after_last; ++i)`. Note that if `index_after_last <= first_index`, no blocks will be submitted.
+ * @param loop The function to loop through. Will be called once per index, many times per block. Should take exactly one argument: the loop index.
+ * @param num_blocks The maximum number of blocks to split the loop into. The default is 0, which means the number of blocks will be equal to the number of threads in the pool.
+ * @param priority The priority of the tasks. Should be between -32,768 and 32,767 (a signed 16-bit integer). The default is 0. Only enabled if `BS_THREAD_POOL_ENABLE_PRIORITY` is defined.
+ */
+ template
+ void detach_loop(const T first_index, const T index_after_last, F&& loop, const size_t num_blocks = 0 BS_THREAD_POOL_PRIORITY_INPUT)
+ {
+ if (index_after_last > first_index)
+ {
+ const blocks blks(first_index, index_after_last, num_blocks ? num_blocks : thread_count);
+ for (size_t blk = 0; blk < blks.get_num_blocks(); ++blk)
+ detach_task(
+ [loop = std::forward(loop), start = blks.start(blk), end = blks.end(blk)]
+ {
+ for (T i = start; i < end; ++i)
+ loop(i);
+ } BS_THREAD_POOL_PRIORITY_OUTPUT);
+ }
+ }
+
+ /**
+ * @brief Submit a sequence of tasks enumerated by indices to the queue, with the specified priority. Does not return a `multi_future`, so the user must use `wait()` or some other method to ensure that the sequence finishes executing, otherwise bad things will happen.
+ *
+ * @tparam T The type of the indices. Should be a signed or unsigned integer.
+ * @tparam F The type of the function used to define the sequence.
+ * @param first_index The first index in the sequence.
+ * @param index_after_last The index after the last index in the sequence. The sequence will iterate from `first_index` to `(index_after_last - 1)` inclusive. In other words, it will be equivalent to `for (T i = first_index; i < index_after_last; ++i)`. Note that if `index_after_last <= first_index`, no tasks will be submitted.
+ * @param sequence The function used to define the sequence. Will be called once per index. Should take exactly one argument, the index.
+ * @param priority The priority of the tasks. Should be between -32,768 and 32,767 (a signed 16-bit integer). The default is 0. Only enabled if `BS_THREAD_POOL_ENABLE_PRIORITY` is defined.
+ */
+ template