diff --git a/.clang-format b/.clang-format index 53dfbad..9084f79 100644 --- a/.clang-format +++ b/.clang-format @@ -25,7 +25,6 @@ AllowShortLambdasOnASingleLine: Empty AllowShortLoopsOnASingleLine: false AlwaysBreakAfterReturnType: None AlwaysBreakBeforeMultilineStrings: false -AlwaysBreakTemplateDeclarations: Yes AttributeMacros: [] BinPackArguments: true BinPackParameters: true @@ -45,7 +44,7 @@ BraceWrapping: BeforeLambdaBody: true BeforeWhile: false IndentBraces: false - SplitEmptyFunction: true + SplitEmptyFunction: false SplitEmptyNamespace: true SplitEmptyRecord: true BracedInitializerIndentWidth: 4 @@ -57,12 +56,11 @@ BreakBeforeConceptDeclarations: true BreakBeforeInheritanceComma: false BreakBeforeTernaryOperators: true BreakConstructorInitializers: BeforeColon -BreakConstructorInitializersBeforeComma: false BreakInheritanceList: BeforeColon BreakStringLiterals: true -ColumnLimit: 1024 +BreakTemplateDeclarations: No +ColumnLimit: 0 CompactNamespaces: false -ConstructorInitializerAllOnOneLineOrOnePerLine: false ConstructorInitializerIndentWidth: 4 ContinuationIndentWidth: 4 Cpp11BracedListStyle: true @@ -99,7 +97,7 @@ ObjCBreakBeforeNestedBlockParam: true ObjCSpaceAfterProperty: false ObjCSpaceBeforeProtocolList: true PPIndentWidth: -1 -PackConstructorInitializers: BinPack +PackConstructorInitializers: CurrentLine PenaltyBreakAssignment: 0 PenaltyBreakBeforeFirstCallParameter: 0 PenaltyBreakComment: 0 diff --git a/.clang-tidy b/.clang-tidy index ea4b7ac..032b1eb 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,7 +1,8 @@ CheckOptions: + cppcoreguidelines-pro-type-member-init.IgnoreArrays: true + cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor: true misc-const-correctness.WarnPointersAsValues: true misc-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic: true - readability-magic-numbers.IgnoredIntegerValues: 0; 1; 2; 3; 4; 5; 6; 7; 8; 9; 10 Checks: > *, -abseil-*, @@ -11,6 +12,10 @@ Checks: > -bugprone-easily-swappable-parameters, -bugprone-empty-catch, -cert-err58-cpp, + -clang-analyzer-alpha.*, + -clang-analyzer-debug.*, + -clang-analyzer-fuchsia.*, + -clang-analyzer-webkit.*, -cppcoreguidelines-avoid-c-arrays, -cppcoreguidelines-avoid-do-while, -cppcoreguidelines-avoid-magic-numbers, @@ -26,15 +31,16 @@ Checks: > -google-*, -hicpp-avoid-c-arrays, -hicpp-braces-around-statements, + -hicpp-member-init, -hicpp-no-array-decay, -hicpp-signed-bitwise, + -hicpp-special-member-functions, -hicpp-use-auto, -hicpp-vararg, -linuxkernel-*, -llvm-*, -llvmlibc-*, -misc-definitions-in-headers, - -misc-use-internal-linkage, -modernize-avoid-bind, -modernize-avoid-c-arrays, -modernize-use-auto, @@ -51,6 +57,8 @@ Checks: > -readability-braces-around-statements, -readability-function-cognitive-complexity, -readability-identifier-length, + -readability-magic-numbers, + -readability-use-concise-preprocessor-directives, -zircon-*, HeaderFileExtensions: [h, hpp, cppm] HeaderFilterRegex: .* diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 1fd9cab..480b5c4 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -32,4 +32,4 @@ What behavior did you expect to get? What actually happened? If the code failed **Additional information** -Include any additional information here. +Include any additional information here. diff --git a/.github/ISSUE_TEMPLATE/failed-tests.md b/.github/ISSUE_TEMPLATE/failed-tests.md index f6e59b9..a4ff3f9 100644 --- a/.github/ISSUE_TEMPLATE/failed-tests.md +++ b/.github/ISSUE_TEMPLATE/failed-tests.md @@ -21,4 +21,4 @@ Please attach the log file generated by the automated test program to this issue **Additional information** -Include any additional information here. +Include any additional information here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index ac7e0a2..2c719cc 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -18,4 +18,4 @@ An example of code that utilizes the suggested feature. Paste or write it betwee **Additional information** -Include any additional information here. +Include any additional information here. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 0e18f48..36e35fb 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -27,4 +27,4 @@ If so, please provide information about the test system(s): **Additional information** -Include any additional information here. +Include any additional information here. diff --git a/.gitignore b/.gitignore index c6adf5c..0b7dd2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ build temp +default_args.txt diff --git a/.vscode-linux/c_cpp_properties.json b/.vscode-linux/c_cpp_properties.json index b86215e..7b65c25 100644 --- a/.vscode-linux/c_cpp_properties.json +++ b/.vscode-linux/c_cpp_properties.json @@ -14,7 +14,7 @@ "BS_THREAD_POOL_NATIVE_EXTENSIONS" ], "includePath": [ - "${workspaceFolder}/**" + "${workspaceFolder}/include" ], "intelliSenseMode": "linux-clang-x64", "name": "Clang C++17" @@ -33,7 +33,7 @@ "BS_THREAD_POOL_NATIVE_EXTENSIONS" ], "includePath": [ - "${workspaceFolder}/**" + "${workspaceFolder}/include" ], "intelliSenseMode": "linux-clang-x64", "name": "Clang C++20" @@ -52,7 +52,7 @@ "BS_THREAD_POOL_NATIVE_EXTENSIONS" ], "includePath": [ - "${workspaceFolder}/**" + "${workspaceFolder}/include" ], "intelliSenseMode": "linux-clang-x64", "name": "Clang C++23" @@ -68,7 +68,7 @@ "BS_THREAD_POOL_NATIVE_EXTENSIONS" ], "includePath": [ - "${workspaceFolder}/**" + "${workspaceFolder}/include" ], "intelliSenseMode": "linux-gcc-x64", "name": "GCC C++17" @@ -84,7 +84,7 @@ "BS_THREAD_POOL_NATIVE_EXTENSIONS" ], "includePath": [ - "${workspaceFolder}/**" + "${workspaceFolder}/include" ], "intelliSenseMode": "linux-gcc-x64", "name": "GCC C++20" @@ -100,7 +100,7 @@ "BS_THREAD_POOL_NATIVE_EXTENSIONS" ], "includePath": [ - "${workspaceFolder}/**" + "${workspaceFolder}/include" ], "intelliSenseMode": "linux-gcc-x64", "name": "GCC C++23" diff --git a/.vscode-linux/launch.json b/.vscode-linux/launch.json index afe98e5..42186ce 100644 --- a/.vscode-linux/launch.json +++ b/.vscode-linux/launch.json @@ -1,59 +1,59 @@ -{ - "configurations": [ - { - "args": [], - "cwd": "${workspaceFolder}${/}build", - "name": "Build and debug (Clang C++17)", - "preLaunchTask": "Build for debugging (Clang C++17)", - "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-clang-cpp17", - "request": "launch", - "type": "lldb" - }, - { - "args": [], - "cwd": "${workspaceFolder}${/}build", - "name": "Build and debug (Clang C++20)", - "preLaunchTask": "Build for debugging (Clang C++20)", - "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-clang-cpp20", - "request": "launch", - "type": "lldb" - }, - { - "args": [], - "cwd": "${workspaceFolder}${/}build", - "name": "Build and debug (Clang C++23)", - "preLaunchTask": "Build for debugging (Clang C++23)", - "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-clang-cpp23", - "request": "launch", - "type": "lldb" - }, - { - "args": [], - "cwd": "${workspaceFolder}${/}build", - "name": "Build and debug (GCC C++17)", - "preLaunchTask": "Build for debugging (GCC C++17)", - "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-gcc-cpp17", - "request": "launch", - "type": "cppdbg" - }, - { - "args": [], - "cwd": "${workspaceFolder}${/}build", - "name": "Build and debug (GCC C++20)", - "preLaunchTask": "Build for debugging (GCC C++20)", - "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-gcc-cpp20", - "request": "launch", - "type": "cppdbg" - }, - { - "args": [], - "cwd": "${workspaceFolder}${/}build", - "name": "Build and debug (GCC C++23)", - "preLaunchTask": "Build for debugging (GCC C++23)", - "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-gcc-cpp23", - "request": "launch", - "type": "cppdbg" - } - ], - "version": "0.2.0" -} +{ + "configurations": [ + { + "args": [], + "cwd": "${workspaceFolder}${/}build", + "name": "Build and debug (Clang C++17)", + "preLaunchTask": "Build for debugging (Clang C++17)", + "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-clang-cpp17", + "request": "launch", + "type": "lldb" + }, + { + "args": [], + "cwd": "${workspaceFolder}${/}build", + "name": "Build and debug (Clang C++20)", + "preLaunchTask": "Build for debugging (Clang C++20)", + "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-clang-cpp20", + "request": "launch", + "type": "lldb" + }, + { + "args": [], + "cwd": "${workspaceFolder}${/}build", + "name": "Build and debug (Clang C++23)", + "preLaunchTask": "Build for debugging (Clang C++23)", + "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-clang-cpp23", + "request": "launch", + "type": "lldb" + }, + { + "args": [], + "cwd": "${workspaceFolder}${/}build", + "name": "Build and debug (GCC C++17)", + "preLaunchTask": "Build for debugging (GCC C++17)", + "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-gcc-cpp17", + "request": "launch", + "type": "cppdbg" + }, + { + "args": [], + "cwd": "${workspaceFolder}${/}build", + "name": "Build and debug (GCC C++20)", + "preLaunchTask": "Build for debugging (GCC C++20)", + "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-gcc-cpp20", + "request": "launch", + "type": "cppdbg" + }, + { + "args": [], + "cwd": "${workspaceFolder}${/}build", + "name": "Build and debug (GCC C++23)", + "preLaunchTask": "Build for debugging (GCC C++23)", + "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-gcc-cpp23", + "request": "launch", + "type": "cppdbg" + } + ], + "version": "0.2.0" +} diff --git a/.vscode-linux/settings.json b/.vscode-linux/settings.json new file mode 100644 index 0000000..f7f9824 --- /dev/null +++ b/.vscode-linux/settings.json @@ -0,0 +1,6 @@ +{ + "C_Cpp.codeAnalysis.exclude": { + "misc/**": true, + "temp/**": true + } +} diff --git a/.vscode-linux/tasks.json b/.vscode-linux/tasks.json index dc945a2..211e95f 100644 --- a/.vscode-linux/tasks.json +++ b/.vscode-linux/tasks.json @@ -470,8 +470,9 @@ }, { "args": [ - "scripts/clear_folder.py", - "../build" + "scripts/compile_cpp.py", + "-b", + "-v" ], "command": "python3", "detail": "Delete all files in the build folder.", @@ -490,13 +491,16 @@ }, { "args": [ - "scripts/clear_folder.py", - "../temp" + "scripts/compile_cpp.py", + "${file}", + "-t=release", + "-y", + "-v" ], "command": "python3", - "detail": "Delete all files in the temp folder.", + "detail": "Compile active file using all available compilers and all relevant C++ standards with warning and optimization flags.", "group": "test", - "label": "Clear temp folder", + "label": "Build all optimized (all compilers and standards)", "presentation": { "clear": false, "echo": true, @@ -510,12 +514,17 @@ }, { "args": [ - "scripts/test_all.py" + "scripts/compile_cpp.py", + "${file}", + "-t=release", + "-y", + "-r", + "-v" ], "command": "python3", - "detail": "Compile and run the test program with all available compilers and all relevant C++ standards.", + "detail": "Compile active file using all available compilers and all relevant C++ standards with warning and optimization flags and run the program.", "group": "test", - "label": "Run all tests", + "label": "Build all optimized and run (all compilers and standards)", "presentation": { "clear": false, "echo": true, diff --git a/.vscode-macos/c_cpp_properties.json b/.vscode-macos/c_cpp_properties.json index c5c0821..1be5f25 100644 --- a/.vscode-macos/c_cpp_properties.json +++ b/.vscode-macos/c_cpp_properties.json @@ -8,7 +8,7 @@ "BS_THREAD_POOL_NATIVE_EXTENSIONS" ], "includePath": [ - "${workspaceFolder}/**" + "${workspaceFolder}/include" ], "intelliSenseMode": "macos-clang-x64", "name": "Clang C++17" @@ -21,7 +21,7 @@ "BS_THREAD_POOL_NATIVE_EXTENSIONS" ], "includePath": [ - "${workspaceFolder}/**" + "${workspaceFolder}/include" ], "intelliSenseMode": "macos-clang-x64", "name": "Clang C++20" @@ -34,7 +34,7 @@ "BS_THREAD_POOL_NATIVE_EXTENSIONS" ], "includePath": [ - "${workspaceFolder}/**" + "${workspaceFolder}/include" ], "intelliSenseMode": "macos-clang-x64", "name": "Clang C++23" diff --git a/.vscode-macos/launch.json b/.vscode-macos/launch.json index e26fabc..b671279 100644 --- a/.vscode-macos/launch.json +++ b/.vscode-macos/launch.json @@ -1,32 +1,32 @@ -{ - "configurations": [ - { - "args": [], - "cwd": "${workspaceFolder}${/}build", - "name": "Build and debug (Clang C++17)", - "preLaunchTask": "Build for debugging (Clang C++17)", - "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-clang-cpp17", - "request": "launch", - "type": "lldb" - }, - { - "args": [], - "cwd": "${workspaceFolder}${/}build", - "name": "Build and debug (Clang C++20)", - "preLaunchTask": "Build for debugging (Clang C++20)", - "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-clang-cpp20", - "request": "launch", - "type": "lldb" - }, - { - "args": [], - "cwd": "${workspaceFolder}${/}build", - "name": "Build and debug (Clang C++23)", - "preLaunchTask": "Build for debugging (Clang C++23)", - "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-clang-cpp23", - "request": "launch", - "type": "lldb" - } - ], - "version": "0.2.0" -} +{ + "configurations": [ + { + "args": [], + "cwd": "${workspaceFolder}${/}build", + "name": "Build and debug (Clang C++17)", + "preLaunchTask": "Build for debugging (Clang C++17)", + "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-clang-cpp17", + "request": "launch", + "type": "lldb" + }, + { + "args": [], + "cwd": "${workspaceFolder}${/}build", + "name": "Build and debug (Clang C++20)", + "preLaunchTask": "Build for debugging (Clang C++20)", + "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-clang-cpp20", + "request": "launch", + "type": "lldb" + }, + { + "args": [], + "cwd": "${workspaceFolder}${/}build", + "name": "Build and debug (Clang C++23)", + "preLaunchTask": "Build for debugging (Clang C++23)", + "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-clang-cpp23", + "request": "launch", + "type": "lldb" + } + ], + "version": "0.2.0" +} diff --git a/.vscode-macos/settings.json b/.vscode-macos/settings.json new file mode 100644 index 0000000..f7f9824 --- /dev/null +++ b/.vscode-macos/settings.json @@ -0,0 +1,6 @@ +{ + "C_Cpp.codeAnalysis.exclude": { + "misc/**": true, + "temp/**": true + } +} diff --git a/.vscode-macos/tasks.json b/.vscode-macos/tasks.json index dd5d118..753ddd8 100644 --- a/.vscode-macos/tasks.json +++ b/.vscode-macos/tasks.json @@ -236,8 +236,9 @@ }, { "args": [ - "scripts/clear_folder.py", - "../build" + "scripts/compile_cpp.py", + "-b", + "-v" ], "command": "python3", "detail": "Delete all files in the build folder.", @@ -256,13 +257,16 @@ }, { "args": [ - "scripts/clear_folder.py", - "../temp" + "scripts/compile_cpp.py", + "${file}", + "-t=release", + "-y", + "-v" ], "command": "python3", - "detail": "Delete all files in the temp folder.", + "detail": "Compile active file using all available compilers and all relevant C++ standards with warning and optimization flags.", "group": "test", - "label": "Clear temp folder", + "label": "Build all optimized (all compilers and standards)", "presentation": { "clear": false, "echo": true, @@ -276,12 +280,17 @@ }, { "args": [ - "scripts/test_all.py" + "scripts/compile_cpp.py", + "${file}", + "-t=release", + "-y", + "-r", + "-v" ], "command": "python3", - "detail": "Compile and run the test program with all available compilers and all relevant C++ standards.", + "detail": "Compile active file using all available compilers and all relevant C++ standards with warning and optimization flags and run the program.", "group": "test", - "label": "Run all tests", + "label": "Build all optimized and run (all compilers and standards)", "presentation": { "clear": false, "echo": true, diff --git a/.vscode-windows/c_cpp_properties.json b/.vscode-windows/c_cpp_properties.json index 9e79264..a12fe30 100644 --- a/.vscode-windows/c_cpp_properties.json +++ b/.vscode-windows/c_cpp_properties.json @@ -14,7 +14,7 @@ "BS_THREAD_POOL_NATIVE_EXTENSIONS" ], "includePath": [ - "${workspaceFolder}/**" + "${workspaceFolder}/include" ], "intelliSenseMode": "windows-clang-x64", "name": "Clang C++17" @@ -33,7 +33,7 @@ "BS_THREAD_POOL_NATIVE_EXTENSIONS" ], "includePath": [ - "${workspaceFolder}/**" + "${workspaceFolder}/include" ], "intelliSenseMode": "windows-clang-x64", "name": "Clang C++20" @@ -52,7 +52,7 @@ "BS_THREAD_POOL_NATIVE_EXTENSIONS" ], "includePath": [ - "${workspaceFolder}/**" + "${workspaceFolder}/include" ], "intelliSenseMode": "windows-clang-x64", "name": "Clang C++23" @@ -68,7 +68,7 @@ "BS_THREAD_POOL_NATIVE_EXTENSIONS" ], "includePath": [ - "${workspaceFolder}/**" + "${workspaceFolder}/include" ], "intelliSenseMode": "windows-gcc-x64", "name": "GCC C++17" @@ -84,7 +84,7 @@ "BS_THREAD_POOL_NATIVE_EXTENSIONS" ], "includePath": [ - "${workspaceFolder}/**" + "${workspaceFolder}/include" ], "intelliSenseMode": "windows-gcc-x64", "name": "GCC C++20" @@ -100,7 +100,7 @@ "BS_THREAD_POOL_NATIVE_EXTENSIONS" ], "includePath": [ - "${workspaceFolder}/**" + "${workspaceFolder}/include" ], "intelliSenseMode": "windows-gcc-x64", "name": "GCC C++23" @@ -116,7 +116,7 @@ "BS_THREAD_POOL_NATIVE_EXTENSIONS" ], "includePath": [ - "${workspaceFolder}/**" + "${workspaceFolder}/include" ], "intelliSenseMode": "windows-msvc-x64", "name": "MSVC C++17" @@ -132,7 +132,7 @@ "BS_THREAD_POOL_NATIVE_EXTENSIONS" ], "includePath": [ - "${workspaceFolder}/**" + "${workspaceFolder}/include" ], "intelliSenseMode": "windows-msvc-x64", "name": "MSVC C++20" @@ -148,7 +148,7 @@ "BS_THREAD_POOL_NATIVE_EXTENSIONS" ], "includePath": [ - "${workspaceFolder}/**" + "${workspaceFolder}/include" ], "intelliSenseMode": "windows-msvc-x64", "name": "MSVC C++23" diff --git a/.vscode-windows/launch.json b/.vscode-windows/launch.json index a869852..f03b556 100644 --- a/.vscode-windows/launch.json +++ b/.vscode-windows/launch.json @@ -1,89 +1,89 @@ -{ - "configurations": [ - { - "args": [], - "cwd": "${workspaceFolder}${/}build", - "name": "Build and debug (Clang C++17)", - "preLaunchTask": "Build for debugging (Clang C++17)", - "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-clang-cpp17", - "request": "launch", - "type": "lldb" - }, - { - "args": [], - "cwd": "${workspaceFolder}${/}build", - "name": "Build and debug (Clang C++20)", - "preLaunchTask": "Build for debugging (Clang C++20)", - "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-clang-cpp20", - "request": "launch", - "type": "lldb" - }, - { - "args": [], - "cwd": "${workspaceFolder}${/}build", - "name": "Build and debug (Clang C++23)", - "preLaunchTask": "Build for debugging (Clang C++23)", - "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-clang-cpp23", - "request": "launch", - "type": "lldb" - }, - { - "args": [], - "cwd": "${workspaceFolder}${/}build", - "name": "Build and debug (GCC C++17)", - "preLaunchTask": "Build for debugging (GCC C++17)", - "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-gcc-cpp17", - "request": "launch", - "type": "cppdbg" - }, - { - "args": [], - "cwd": "${workspaceFolder}${/}build", - "name": "Build and debug (GCC C++20)", - "preLaunchTask": "Build for debugging (GCC C++20)", - "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-gcc-cpp20", - "request": "launch", - "type": "cppdbg" - }, - { - "args": [], - "cwd": "${workspaceFolder}${/}build", - "name": "Build and debug (GCC C++23)", - "preLaunchTask": "Build for debugging (GCC C++23)", - "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-gcc-cpp23", - "request": "launch", - "type": "cppdbg" - }, - { - "args": [], - "console": "integratedTerminal", - "cwd": "${workspaceFolder}${/}build", - "name": "Build and debug (MSVC C++17)", - "preLaunchTask": "Build for debugging (MSVC C++17)", - "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-msvc-cpp17", - "request": "launch", - "type": "cppvsdbg" - }, - { - "args": [], - "console": "integratedTerminal", - "cwd": "${workspaceFolder}${/}build", - "name": "Build and debug (MSVC C++20)", - "preLaunchTask": "Build for debugging (MSVC C++20)", - "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-msvc-cpp20", - "request": "launch", - "type": "cppvsdbg" - }, - { - "args": [], - "console": "integratedTerminal", - "cwd": "${workspaceFolder}${/}build", - "name": "Build and debug (MSVC C++23)", - "preLaunchTask": "Build for debugging (MSVC C++23)", - "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-msvc-cpp23", - "request": "launch", - "type": "cppvsdbg" - } - ], - "version": "0.2.0" -} +{ + "configurations": [ + { + "args": [], + "cwd": "${workspaceFolder}${/}build", + "name": "Build and debug (Clang C++17)", + "preLaunchTask": "Build for debugging (Clang C++17)", + "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-clang-cpp17", + "request": "launch", + "type": "lldb" + }, + { + "args": [], + "cwd": "${workspaceFolder}${/}build", + "name": "Build and debug (Clang C++20)", + "preLaunchTask": "Build for debugging (Clang C++20)", + "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-clang-cpp20", + "request": "launch", + "type": "lldb" + }, + { + "args": [], + "cwd": "${workspaceFolder}${/}build", + "name": "Build and debug (Clang C++23)", + "preLaunchTask": "Build for debugging (Clang C++23)", + "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-clang-cpp23", + "request": "launch", + "type": "lldb" + }, + { + "args": [], + "cwd": "${workspaceFolder}${/}build", + "name": "Build and debug (GCC C++17)", + "preLaunchTask": "Build for debugging (GCC C++17)", + "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-gcc-cpp17", + "request": "launch", + "type": "cppdbg" + }, + { + "args": [], + "cwd": "${workspaceFolder}${/}build", + "name": "Build and debug (GCC C++20)", + "preLaunchTask": "Build for debugging (GCC C++20)", + "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-gcc-cpp20", + "request": "launch", + "type": "cppdbg" + }, + { + "args": [], + "cwd": "${workspaceFolder}${/}build", + "name": "Build and debug (GCC C++23)", + "preLaunchTask": "Build for debugging (GCC C++23)", + "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-gcc-cpp23", + "request": "launch", + "type": "cppdbg" + }, + { + "args": [], + "console": "integratedTerminal", + "cwd": "${workspaceFolder}${/}build", + "name": "Build and debug (MSVC C++17)", + "preLaunchTask": "Build for debugging (MSVC C++17)", + "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-msvc-cpp17", + "request": "launch", + "type": "cppvsdbg" + }, + { + "args": [], + "console": "integratedTerminal", + "cwd": "${workspaceFolder}${/}build", + "name": "Build and debug (MSVC C++20)", + "preLaunchTask": "Build for debugging (MSVC C++20)", + "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-msvc-cpp20", + "request": "launch", + "type": "cppvsdbg" + }, + { + "args": [], + "console": "integratedTerminal", + "cwd": "${workspaceFolder}${/}build", + "name": "Build and debug (MSVC C++23)", + "preLaunchTask": "Build for debugging (MSVC C++23)", + "program": "${workspaceFolder}${/}build${/}${fileBasenameNoExtension}_debug-msvc-cpp23", + "request": "launch", + "type": "cppvsdbg" + } + ], + "version": "0.2.0" +} diff --git a/.vscode-windows/settings.json b/.vscode-windows/settings.json new file mode 100644 index 0000000..76817cb --- /dev/null +++ b/.vscode-windows/settings.json @@ -0,0 +1,9 @@ +{ + "C_Cpp.codeAnalysis.clangTidy.args": [ + "--extra-arg-before=--target=x86_64-pc-windows-msvc", + ], + "C_Cpp.codeAnalysis.exclude": { + "misc/**": true, + "temp/**": true + } +} diff --git a/.vscode-windows/tasks.json b/.vscode-windows/tasks.json index ee2e307..6c9d901 100644 --- a/.vscode-windows/tasks.json +++ b/.vscode-windows/tasks.json @@ -704,8 +704,9 @@ }, { "args": [ - "scripts/clear_folder.py", - "../build" + "scripts/compile_cpp.py", + "-b", + "-v" ], "command": "python", "detail": "Delete all files in the build folder.", @@ -724,13 +725,16 @@ }, { "args": [ - "scripts/clear_folder.py", - "../temp" + "scripts/compile_cpp.py", + "${file}", + "-t=release", + "-y", + "-v" ], "command": "python", - "detail": "Delete all files in the temp folder.", + "detail": "Compile active file using all available compilers and all relevant C++ standards with warning and optimization flags.", "group": "test", - "label": "Clear temp folder", + "label": "Build all optimized (all compilers and standards)", "presentation": { "clear": false, "echo": true, @@ -744,12 +748,17 @@ }, { "args": [ - "scripts/test_all.py" + "scripts/compile_cpp.py", + "${file}", + "-t=release", + "-y", + "-r", + "-v" ], "command": "python", - "detail": "Compile and run the test program with all available compilers and all relevant C++ standards.", + "detail": "Compile active file using all available compilers and all relevant C++ standards with warning and optimization flags and run the program.", "group": "test", - "label": "Run all tests", + "label": "Build all optimized and run (all compilers and standards)", "presentation": { "clear": false, "echo": true, diff --git a/CHANGELOG.md b/CHANGELOG.md index 0141d89..90d6147 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Website: \ GitHub: * [Version history](#version-history) + * [v5.1.0 (2026-01-03)](#v510-2026-01-03) * [v5.0.0 (2024-12-19)](#v500-2024-12-19) * [v4.1.0 (2024-03-22)](#v410-2024-03-22) * [v4.0.1 (2023-12-28)](#v401-2023-12-28) @@ -30,6 +31,50 @@ GitHub: ## Version history +### v5.1.0 (2026-01-03) + +* New/changed features: + * Added `detach_bulk()` and `submit_bulk()` member functions to submit tasks in bulk. You can pass either a range of iterators or a container. The mutex protecting the task queue is locked only once for the entire bulk submission, which should improve performance when submitting a large number of tasks; if the tasks were submitted using individual calls to `detach_task()` or `submit_task()` instead, the mutex would need to be locked and unlocked for each task. + * `detach_blocks()`/`detach_loop()`/`detach_sequence()` and `submit_blocks()`/`submit_loop()`/`submit_sequence()` now use `detach_bulk()` and `submit_bulk()`, respectively, under the hood for increased performance (the API remains the same). They have also been refactored using helper functions and custom function object classes to reduce code duplication. + * All `submit_*` member functions now use a C++17/20 polyfill for `std::move_only_function` when C++23 is not available (or when using libc++, which at the time of this release has not implemented it yet). This allows them to work without using `std::shared_ptr`, which should increase performance. + * If the native extensions are enabled, a pool created with the default constructor will now only use the number of threads available to the process, as obtained from `BS::get_os_process_affinity()`, which can be less than the number of hardware threads. See [#161](https://github.com/bshoshany/thread-pool/issues/161). + * Since importing the C++ Standard Library using `import std` is now supported by all 3 major compilers, the library (and test program) will now use `import std` whenever the macro `BS_THREAD_POOL_IMPORT_STD` is defined, as long as C++23 is enabled, without performing any additional checks for compiler or standard library support (aside from the workaround for GCC mentioned below). + * The `BS::tp` enumeration is now properly defined as an `enum class` with the appropriate bitwise operators. + * Removed the polyfills for `std::counting_semaphore` and `std::binary_semaphore` from `BS_thread_pool.hpp`, as they are not used by the library itself, to reduce the size of the header file. If you need them, you can copy them from the test program `BS_thread_pool_test.cpp`. + * On Windows, if the native extensions are enabled, `WIN32_LEAN_AND_MEAN` is now defined before including `` to reduce compilation time. +* Bug fixes: + * The system macros `major` and `minor` (from `` on Linux) or `min` and `max` (from `` on Windows) are now automatically undefined if they are detected, to prevent compilation errors. This was also done previously, but only under certain conditions; now it is done unconditionally, as some users reported issues. On Windows, `NOMINMAX` is now defined before including ``, but the `min` and `max` macros are still undefined independently. + * `BS::wait_deadlock` is now only exported by the module if exceptions are enabled, preventing a compiler error. See [#160](https://github.com/bshoshany/thread-pool/pull/160). + * Fixed a typo in `BS::thread_pool::submit_sequence()` which caused the wrong number of futures to be reserved, potentially resulting in unnecessary reallocations. + * `BS::multi_future::wait_until` now correctly waits using the specific clock type passed in the template parameter. + * Fixed a bug where `reset()` failed to notify worker threads if the pool was unpaused before resetting. + * `get_os_thread_affinity()` and `set_os_thread_affinity()` now return `std::nullopt` and `false` (respectively) on Android, as the API is not supported. See [#163](https://github.com/bshoshany/thread-pool/pull/163). +* Tests: + * The test program `BS_thread_pool_test.cpp` now prints colored output for better readability. This can be disabled by setting the `NO_COLOR` environment variable. + * The test program now prints out the Mandelbrot set it generates, downsampled to fit in a terminal window (at 120 character width). This will be in 24-bit color in the terminal, and in monochrome using Unicode blocks in the log file. (If `NO_COLOR` is set, the terminal output will also be in monochrome.) + * The test program now looks for `default_args.txt` in both the current folder and the parent folder when reading default command line arguments. +* Documentation: + * Added an example in `README.md` for getting and setting process affinity. + * Removed the suggestion in `README.md` to use the `-pthread` flag on Linux/macOS, as it does not seem to be necessary in order to use the library, at least on the systems I tested with. If you are using a system that requires it, then you probably already know about it. + * Updated the instructions in `README.md` for compiling with GCC using `import BS.thread_pool`, and added instructions on how to compile the `std` module with GNU libstdc++. + * Fixed many typos and inconsistencies in `README.md`. +* Known issues: + * At the time of this release, there is a bug in Clang with libc++ where using `std::jthread` in a C++20 module causes a compilation error. As a workaround, until the bug is fixed, the thread pool library automatically falls back to `std::thread` if it detects that Clang and libc++ are being used together with C++20 modules. This workaround can be disabled by defining `BS_THREAD_POOL_DISABLE_WORKAROUNDS` when compiling the module. + * At the time of this release, there is a bug when using GCC with libstdc++ on Windows via MSYS2 where the `BS.thread_pool` module doesn't compile if both native extensions and `import std` are enabled. As a workaround, until the bug is fixed, the thread pool library automatically falls back to header files if it detects that GCC and libstdc++ are being used together with the C++23 `std` module on Windows. This workaround can be disabled by defining `BS_THREAD_POOL_DISABLE_WORKAROUNDS` when compiling the module. +* Development: + * The Python script I use for compiling the test program, `compile_cpp.py`, has received numerous improvements: + * The script now supports `import std` with GCC in addition to Clang and MSVC. + * The script now only recompiles the program if the source file(s), module(s), and optional additional dependencies (added using the flag `-n`/`--deps` or the `deps` field in `compile_cpp.yaml`) have changed since the binary was created. Use `-e`/`--force` to force recompilation. + * The script now allows disabling exceptions by either defining `disable_exceptions: true` in `compile_cpp.yaml` or using the flag `-x=true`/`--disable-exceptions=true` (the flag overrides the YAML file). + * The script can now compile modules independently using the `-l`/`--as-module` flag. + * The script can now clear the output folder using the `-b`/`--clear-output` flag (replaces the `clear_folder.py` script from the previous release). + * The script can now test compilation using all possible combinations of compilers and C++ standards available in the system using the `-y`/`--try-all` flag (replaces the `test_all.py` script from the previous release). + * The script now automatically detects the Visual Studio installation path on Windows. + * The script now implements a more robust mechanism for finding the `std` module. + * The script now prints colored output for better readability. This can be disabled by setting the `NO_COLOR` environment variable. + * The script no longer disables optimizations when compiling with GCC and modules (since that bug is fixed in recent versions of GCC). + * Added a VS Code task to compile the test program (or the active file) with all available compilers and all relevant C++ standards, but without running it, to quickly check for compiler compatibility issues. + ### v5.0.0 (2024-12-19) * A major new release with many new features, improvements, bug fixes, and performance optimizations! Please note that code written using previous releases may 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. @@ -172,7 +217,7 @@ GitHub: * **Development:** * A Python script `compile_cpp.py` has been added to the repository, in the `scripts` folder. It can be used to compile any C++ source file with different compilers on different platforms. The compilation parameters can be configured using command line arguments and/or via an optional YAML configuration file `compile_cpp.yaml` which specifies defined macros, extra compiler flags (per compiler), include folders, modules, and the output folder. * I wrote this script to make it easier for me to test the library with different combinations of compilers, standards, and platforms using the built-in Visual Studio Code tasks. I also included three `.vscode` folders (one for each OS) in the repository, with appropriate `c_cpp_properties.json`, `launch.json`, and `tasks.json` files that utilize this script, in case you want to use it in your own projects. However, note that this script is not meant to replace CMake or any full-fledged build system, it's just a convenient script for developing single-header libraries like this one or other small projects. - * The `compile_cpp.py` script also transparently handles C++20 modules and importing the C++ Standard Library as a module in C++23. Therefore, users of this library who wish to import it as a C+20 module may find this script particularly useful. + * The `compile_cpp.py` script also transparently handles C++20 modules and importing the C++ Standard Library as a module in C++23. Therefore, users of this library who wish to import it as a C++20 module may find this script particularly useful. * Another Python script `test_all.py` in the `scripts` folder replaces the old PowerShell test script. Tests are now performed in C++17, C++20, and C++23 modes, using all compilers available in the system (Clang, GCC, and/or MSVC). Since there are so many tests, the test script now no longer performs the benchmarks, as that would take too long. * A final Python script `clear_folder.py` in the `scripts` folder is used to clean up output and temporary folders, and integrates with VS Code tasks. diff --git a/CITATION.bib b/CITATION.bib index bbba157..652b177 100644 --- a/CITATION.bib +++ b/CITATION.bib @@ -9,4 +9,4 @@ url = {https://www.sciencedirect.com/science/article/pii/S235271102400058X}, volume = {26}, year = {2024} -} +} diff --git a/CITATION.cff b/CITATION.cff index 05e3a98..df3f96a 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -22,4 +22,4 @@ preferred-citation: repository-code: https://github.com/bshoshany/thread-pool title: A C++17 Thread Pool for High-Performance Scientific Computing type: software -url: https://github.com/bshoshany/thread-pool +url: https://github.com/bshoshany/thread-pool diff --git a/LICENSE.txt b/LICENSE.txt index 5a7c645..ea7aa26 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Barak Shoshany +Copyright (c) 2021-2026 Barak Shoshany Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 4794b5a..55315d6 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Email: \ Website: \ GitHub: -This is the complete documentation for **v5.0.0** of the library, released on **2024-12-19**. +This is the complete documentation for **v5.1.0** of the library, released on **2026-01-03**. * [Introduction](#introduction) * [Motivation](#motivation) @@ -43,9 +43,9 @@ This is the complete documentation for **v5.0.0** of the library, released on ** * [Loops with return values](#loops-with-return-values) * [Parallelizing sequences](#parallelizing-sequences) * [More about `BS::multi_future`](#more-about-bsmulti_future) + * [Submitting tasks in bulk without a loop](#submitting-tasks-in-bulk-without-a-loop) * [Utility classes](#utility-classes) * [Synchronizing printing to a stream with `BS::synced_stream`](#synchronizing-printing-to-a-stream-with-bssynced_stream) - * [Synchronizing tasks with `BS::counting_semaphore` and `BS::binary_semaphore`](#synchronizing-tasks-with-bscounting_semaphore-and-bsbinary_semaphore) * [Managing tasks](#managing-tasks) * [Monitoring the tasks](#monitoring-the-tasks) * [Purging tasks](#purging-tasks) @@ -82,12 +82,13 @@ This is the complete documentation for **v5.0.0** of the library, released on ** * [Enabling `import std`](#enabling-import-std) * [Compiling with `compile_cpp.py` using `import std`](#compiling-with-compile_cpppy-using-import-std) * [Compiling with Clang and LLVM libc++ using `import std`](#compiling-with-clang-and-llvm-libc-using-import-std) + * [Compiling with GCC and GNU libstdc++ using `import std`](#compiling-with-gcc-and-gnu-libstdc-using-import-std) * [Compiling with MSVC and Microsoft STL using `import std`](#compiling-with-msvc-and-microsoft-stl-using-import-std) * [Compiling with CMake using `import std`](#compiling-with-cmake-using-import-std) * [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 Conan](#installing-using-conan) * [Installing using CMake with CPM](#installing-using-cmake-with-cpm) * [Installing using CMake with `FetchContent`](#installing-using-cmake-with-fetchcontent) * [Complete library reference](#complete-library-reference) @@ -102,7 +103,7 @@ This is the complete documentation for **v5.0.0** of the library, released on ** * [All names exported by the C++20 module](#all-names-exported-by-the-c20-module) * [Development tools](#development-tools) * [The `compile_cpp.py` script](#the-compile_cpppy-script) - * [Other included tools](#other-included-tools) + * [Visual Studio Code tasks](#visual-studio-code-tasks) * [About the project](#about-the-project) * [Bug reports and feature requests](#bug-reports-and-feature-requests) * [Contribution and pull request policy](#contribution-and-pull-request-policy) @@ -144,10 +145,10 @@ Obtaining the library is quick and easy; it can be downloaded manually from [the * 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 on any platform. - * Only 487 lines of code, including all optional features and utility classes (excluding comments, blank lines, lines containing only a single brace, C++17 polyfills, and native extensions). + * Only 536 lines of code, including all optional features and utility classes (excluding comments, blank lines, lines containing only a single brace, C++17/20 polyfills, and native extensions). * **Modern:** * Fully supports C++17, C++20, and C++23, taking advantage of the latest language features when available for maximum performance, reliability, and usability. - * In C++20, the library can an be imported as a C++20 module using [`import BS.thread_pool`](#importing-the-library-as-a-c20-module), with many benefits, such as faster compilation times and avoiding namespace pollution. + * In C++20, the library can be imported as a C++20 module using [`import BS.thread_pool`](#importing-the-library-as-a-c20-module), with many benefits, such as faster compilation times and avoiding namespace pollution. * In C++23, the library can import the C++ Standard Library as a module using [`import std`](#importing-the-c23-standard-library-as-a-module) on supported compilers and platforms. * Makes use of modern C++ programming practices for readability, maintainability, performance, safety, portability, and reliability. * **Easy to use:** @@ -155,7 +156,7 @@ Obtaining the library is quick and easy; it can be downloaded manually from [the * Every task submitted to the queue using [`submit_task()`](#submitting-tasks-to-the-queue) automatically generates an `std::future`, which can be used to wait for the task to finish executing, obtain its eventual return value, and/or catch any thrown exceptions. * Loops can be automatically parallelized into any number of tasks using [`submit_loop()`](#parallelizing-loops), which returns a [`BS::multi_future`](#more-about-bsmulti_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()`](#detaching-and-waiting-for-tasks), and loops can be parallelized using [`detach_loop()`](#parallelizing-loops-without-futures) - 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. - * Extremely thorough and detailed documentation, with numerous examples, is available in the library's [`README.md` file](https://github.com/bshoshany/thread-pool/blob/master/README.md), with a total of 3,359 lines and 25,506 words! + * Extremely thorough and detailed documentation, with numerous examples, is available in the library's [`README.md` file](https://github.com/bshoshany/thread-pool/blob/master/README.md), with a total of 3,486 lines and 26,700 words! * 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. * Optionally, the included Python script [`compile_cpp.py`](#the-compile_cpppy-script) can be used to easily compile any programs that are using the library, with full support for C++20 modules and C++23 Standard Library modules where applicable. * **Additional features:** @@ -167,10 +168,10 @@ Obtaining the library is quick and easy; it can be downloaded manually from [the * Run a cleanup function in each thread right before it is destroyed, using [`set_cleanup_func()`](#thread-cleanup-functions). * Assume lower-level control of parallelized loops using [`detach_blocks()` and `submit_blocks()`](#parallelizing-individual-indices-vs-blocks). * Parallelize a sequence of tasks enumerated by indices to the queue using [`detach_sequence()` and `submit_sequence()`](#parallelizing-sequences). + * Submit tasks in bulk from a container or iterator range using [`detach_bulk()` and `submit_bulk()`](#submitting-tasks-in-bulk-without-a-loop). * Get [information about the current thread](#getting-information-about-the-current-thread): the pool index using `BS::this_thread::get_index()` and a pointer to the owning pool using `BS::this_thread::get_pool()`. * Get the unique thread IDs for all threads in the pool using [`get_thread_ids()`](#getting-and-resetting-the-number-of-threads-in-the-pool). * Synchronize output to one or more streams from multiple threads in parallel using the [`BS::synced_stream`](#synchronizing-printing-to-a-stream-with-bssynced_stream) utility class. - * Access C++20 semaphores in C++17 using the [`BS::binary_semaphore` and `BS::counting_semaphore`](#synchronizing-tasks-with-bscounting_semaphore-and-bsbinary_semaphore) polyfill classes. * **Optional features:** * [Optional features](#enabling-features) can be enabled by passing a bitmask template parameter to the `BS::thread_pool` class template. * Assign a priority to each task using the optional [task priority](#setting-task-priority) feature. The priority, in the range -128 to +127, is passed as the last argument to all `submit` and `detach` member functions. Tasks with higher priorities will be executed first. @@ -185,9 +186,9 @@ Obtaining the library is quick and easy; it can be downloaded manually from [the * Use [`BS::get_os_process_affinity()` and `BS::set_os_process_affinity()`](#setting-process-affinity) to get and set the processor affinity of the current process. * Get the implementation-defined thread handles for all threads in the pool using [`get_native_handles()`](#accessing-native-thread-handles). * **Well-tested:** - * The included test program [`BS_thread_pool_test.cpp`](#automated-tests) performs hundreds of automated tests, and also serves as a comprehensive example of how to properly use the library. + * The included test program [`BS_thread_pool_test.cpp`](#testing-the-library) performs hundreds of automated tests, and also serves as a comprehensive example of how to properly use the library. * The test program also performs [benchmarks](#performance-tests) using a highly-optimized multithreaded algorithm which generates a plot of the Mandelbrot set. - * The included Python script `test_all.py` provides a portable way to easily run the tests with multiple compilers. + * The included Python script [`compile_cpp.py`](#the-compile_cpppy-script) provides a portable way to automatically run the tests with all available compilers with one command. * [Compatibility](#compiling-and-compatibility) is comprehensively tested on the latest versions of Windows, Ubuntu, and macOS, using Clang, GCC, and MSVC. * 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). @@ -203,7 +204,7 @@ To install `BS::thread_pool`, simply download the [latest release](https://githu 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); no additional files are required, as the library is a single-header library. -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/). Please [see below](#installing-the-library-using-package-managers) for more details. +This library is also available on various package managers and build systems, including [vcpkg](https://vcpkg.io/), [Conan](https://conan.io/), [Meson](https://mesonbuild.com/), and [CMake](https://cmake.org/). Please [see below](#installing-the-library-using-package-managers) for more details. If C++20 features are available, the library can also be imported as a C++20 module, in which case `#include "BS_thread_pool.hpp"` should be replaced with `import BS.thread_pool;`. This requires one additional file, and the module must be compiled before it can be used; please see detailed instructions [below](#importing-the-library-as-a-c20-module). @@ -211,22 +212,22 @@ If C++20 features are available, the library can also be imported as a This library officially supports C++17, C++20, and C++23. If compiled with C++20 and/or C++23 support, the library will make use of newly available features for maximum performance and usability. However, the library is fully compatible with C++17, and 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 using the bundled test program `BS_thread_pool_test.cpp`, compiled using the bundled Python scripts `test_all.py` and `compile_cpp.py` with native extensions enabled, importing the library [as a C++20 module](#importing-the-library-as-a-c20-module) where applicable, and importing the [C++23 Standard Library as a module](#importing-the-c23-standard-library-as-a-module) where applicable, on a 24-core (8P+16E) / 32-thread Intel i9-13900K CPU, using the following compilers, C++ standard libraries, and platforms: +Compatibility was verified using the bundled test program [`BS_thread_pool_test.cpp`](#testing-the-library), compiled using the bundled Python script [`compile_cpp.py`](#the-compile_cpppy-script) with native extensions enabled, importing the library [as a C++20 module](#importing-the-library-as-a-c20-module) where applicable, and importing the [C++23 Standard Library as a module](#importing-the-c23-standard-library-as-a-module) where applicable, on a 24-core (8P+16E) / 32-thread Intel i9-13900K CPU, using the following compilers, C++ standard libraries, and platforms: -* Windows 11 23H2 build 22631.4602: - * [Clang](https://clang.llvm.org/) v19.1.4 with LLVM libc++ v19.1.4 ([MSYS2 build](https://www.msys2.org/)) - * [GCC](https://gcc.gnu.org/) v14.2.0 with GNU libstdc++ v14 (20240801) ([MSYS2 build](https://www.msys2.org/)) - * [MSVC](https://docs.microsoft.com/en-us/cpp/) v19.42.34435 with Microsoft STL v143 (202408). -* Ubuntu 24.10: - * [Clang](https://clang.llvm.org/) v19.1.6 with LLVM libc++ v19.1.6 - * [GCC](https://gcc.gnu.org/) v14.2.0 with GNU libstdc++ v14 (20240908) +* Windows 11 25H2 build 26200.7462: + * [Clang](https://clang.llvm.org/) v21.1.8 with LLVM libc++ v21.1.8 ([MSYS2 build](https://www.msys2.org/)) + * [GCC](https://gcc.gnu.org/) v15.2.0 with GNU libstdc++ v15 (20250808) ([MSYS2 build](https://www.msys2.org/)) + * [MSVC](https://docs.microsoft.com/en-us/cpp/) v19.50.35721 with Microsoft STL v145 (202508). +* Ubuntu 25.10: + * [Clang](https://clang.llvm.org/) v21.1.8 with LLVM libc++ v21.1.8 + * [GCC](https://gcc.gnu.org/) v15.2.0 with GNU libstdc++ v15 (20250917) * macOS 15.1 build 24B83: - * [Clang](https://clang.llvm.org/) v19.1.6 with LLVM libc++ v19.1.6 ([Homebrew build](https://formulae.brew.sh/formula/llvm)) + * [Clang](https://clang.llvm.org/) v21.1.8 with LLVM libc++ v21.1.8 ([Homebrew build](https://formulae.brew.sh/formula/llvm)) * Note: Apple Clang is currently not officially supported, as it does not support C++20 modules. 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 Clang or GCC, use the `-std=c++17` flag. * 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: @@ -234,17 +235,17 @@ For maximum performance, it is recommended to compile with all available compile * For Clang or GCC, use the `-O3` flag. * For MSVC, use `/O2`. -As an example, to compile the test program `BS_thread_pool_test.cpp` with compiler optimizations, it is recommended to use the following commands: +As an example, to compile the test program `BS_thread_pool_test.cpp` with compiler optimizations, first create the `build` folder using `mkdir build`, and then run the following command in the root folder of the repository: * Windows: - * GCC: `g++ BS_thread_pool_test.cpp -std=c++17 -O3 -o BS_thread_pool_test.exe` - * Clang: `clang++ BS_thread_pool_test.cpp -std=c++17 -O3 -o BS_thread_pool_test.exe` - * MSVC: `cl BS_thread_pool_test.cpp /std:c++17 /permissive- /O2 /EHsc /Fo:BS_thread_pool_test.obj /Fe:BS_thread_pool_test.exe` + * GCC: `g++ -std=c++17 -O3 -I include tests/BS_thread_pool_test.cpp -o build/BS_thread_pool_test.exe` + * Clang: `clang++ -std=c++17 -O3 -I include tests/BS_thread_pool_test.cpp -o build/BS_thread_pool_test.exe` + * MSVC: `cl /std:c++17 /permissive- /O2 /EHsc /I include tests/BS_thread_pool_test.cpp /Fo:build/BS_thread_pool_test.obj /Fe:build/BS_thread_pool_test.exe` (in the Visual Studio Developer PowerShell for your CPU architecture) * Linux/macOS: - * GCC: `g++ BS_thread_pool_test.cpp -std=c++17 -O3 -pthread -o BS_thread_pool_test` - * Clang: `clang++ BS_thread_pool_test.cpp -std=c++17 -O3 -pthread -o BS_thread_pool_test` + * GCC: `g++ -std=c++17 -O3 -I include tests/BS_thread_pool_test.cpp -o build/BS_thread_pool_test` + * Clang: `clang++ -std=c++17 -O3 -I include tests/BS_thread_pool_test.cpp -o build/BS_thread_pool_test` -If your compiler and codebase support C++20 and/or C++23, it is recommended to enable them in order to allow the library access to the latest features: +If your compiler and codebase support C++20 and/or C++23, it is recommended to enable them in order to allow the thread pool library access to the latest features: * For Clang or GCC, use the `-std=c++20` or `-std=c++23` flag. * For MSVC, use `/std:c++20` for C++20 or `/std:c++latest` for C++23. @@ -269,15 +270,19 @@ BS::thread_pool pool(12); 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. If this is the case, it is recommended to use the default value for the number of threads. This ensures that all the threads available in the hardware will be put to work while the main thread waits. -However, if the main thread also performs computationally intensive tasks, it may be beneficial to use one fewer thread than the hardware concurrency, leaving one hardware thread available for the main thread. Furthermore, if more than one thread pool is used in the program simultaneously, the total number of thread across all pools should not exceed the hardware concurrency. +However, if the main thread also performs computationally intensive tasks, it may be beneficial to use one fewer thread than the hardware concurrency, leaving one hardware thread available for the main thread. Furthermore, if more than one thread pool is used in the program simultaneously, the total number of threads across all pools should not exceed the hardware concurrency. + +Note: If the [native extensions](#native-extensions) are enabled, a pool created with the default constructor will only use the number of threads available to the process, as obtained from [`BS::get_os_process_affinity()`](#setting-process-affinity), which can be less than the number of hardware threads. ### Getting and resetting the number of threads in the pool -The member function `get_thread_count()` returns the number of threads in the pool. This will be equal to `std::thread::hardware_concurrency()` if the default constructor was used. +The member function `get_thread_count()` returns the number of threads in the pool. This will be equal to `std::thread::hardware_concurrency()` if the default constructor was used (or to [`BS::get_os_process_affinity()`](#setting-process-affinity) if the [native extensions](#native-extensions) are enabled). It is generally unnecessary to change the number of threads in the pool after it has been created, since the whole point of a thread pool is that you only create the threads once. However, if needed, this can be done, safely and on-the-fly, using the `reset()` member function. -`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 newly submitted tasks. +`reset()` will wait for all tasks to be completed, both those that are currently running in the threads and those that are still waiting 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 (if no argument is given, it behaves like the default constructor), with an empty task queue. + +If pausing is enabled ([see below](#pausing-the-pool)), `reset()` will only wait for tasks that are currently running before destroying the pool; once the pool is reset, it will then resume executing the tasks that remained in the queue and any newly submitted tasks. If the pool was paused before resetting it, the new pool will be paused as well. `reset()` can also be used to change the thread initialization function ([see below](#thread-initialization-functions)). 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 to identify and distinguish between threads, or for allocating resources. @@ -313,7 +318,7 @@ int main() } ``` -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 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 the `get()` member function of the future to get the return value, and printed it out. In addition to submitting 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: @@ -364,7 +369,7 @@ Here we split the lambda into multiple lines to make it more readable. The comma ### Submitting tasks with arguments and receiving a future -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. The following is an example of submitting a pre-defined function with arguments by wrapping it in a lambda: +As stated in the previous section, tasks submitted using `submit_task()` cannot have any arguments. However, it is easy to submit tasks with arguments either by wrapping the function in a lambda or using lambda captures directly. The following is an example of submitting a pre-defined function with arguments by wrapping it in a lambda: ```cpp #include "BS_thread_pool.hpp" // BS::thread_pool @@ -545,7 +550,7 @@ Sorry, the task is not done yet. Task done! ``` -For detached tasks, since we do not have futures 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. +For detached tasks, since we do not have futures 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 return `true` if all tasks finished running, or `false` if the duration expired or the time point was reached but some tasks are still running. Here is the same example as above, using `detach_task()` and `pool.wait_for()`: @@ -715,7 +720,7 @@ where: * The loop is over the range `[start, end)`, i.e. inclusive of `start` but exclusive of `end`. * `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 `submit_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 following syntax: ```cpp pool.submit_loop(start, end, loop, num_blocks); @@ -1022,6 +1027,44 @@ However, `BS::multi_future` also has additional member functions that are aim 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. +### Submitting tasks in bulk without a loop + +Sometimes, you may have a large number of tasks to submit to the thread pool, which are not part of a loop or sequence. In such cases, you can use `detach_bulk()` or `submit_bulk()` to submit all the tasks at once. As usual, `detach_bulk()` simply detaches the tasks, while `submit_bulk()` returns a `BS::multi_future`. The two functions can be used in one of two ways: + +1. By passing a container of callable objects. They must have no arguments; to submit functions with arguments, enclose them in lambda expressions. In the case of `submit_bulk()`, the callables may have return values, which will be stored in the returned `BS::multi_future`, but they must all return the same type. +2. By passing an iterator range, similarly to the standard library algorithms. This can be used, for example, to submit only a subset of tasks from a larger container. + +The following example demonstrates how to use `submit_bulk()` with a container: + +```cpp +#include "BS_thread_pool.hpp" // BS::multi_future, BS::thread_pool +#include // std::function +#include // std::cout +#include // std::string +#include // std::vector + +int main() +{ + BS::thread_pool pool; + std::vector> tasks; + tasks.emplace_back([] + { + return "Do something."; + }); + tasks.emplace_back([] + { + return "Do something else."; + }); + tasks.emplace_back([] + { + return "Do another thing."; + }); + BS::multi_future results = pool.submit_bulk(tasks); + for (const std::string& result : results.get()) + std::cout << result << '\n'; +} +``` + ## Utility classes ### Synchronizing printing to a stream with `BS::synced_stream` @@ -1150,7 +1193,7 @@ int main() Note that we must wait on the future before the `main()` function ends, as otherwise the log file may be destructed before the tasks finish executing. If we used `detach_sequence()`, which does not return a future, we would have to call `pool.wait()` in the last line. -In this example we did not create the `BS::synced_stream` as a global object, since we wanted to pass the log file as a stream to the constructor. However, it is also possible to add streams to or remove streams from an existing `BS::synced_stream` object using the member functions `add_stream()` and `remove_stream()`. For example, in the following program, we create a `BS::synced_stream` global object with the default constructor, so that it prints to `std::cout`, but then we change out minds, remove `std::cout` from the list of streams, and add a log file instead: +In this example we did not create the `BS::synced_stream` as a global object, since we wanted to pass the log file as a stream to the constructor. However, it is also possible to add streams to or remove streams from an existing `BS::synced_stream` object using the member functions `add_stream()` and `remove_stream()`. For example, in the following program, we create a `BS::synced_stream` global object with the default constructor, so that it prints to `std::cout`, but then we change our minds, remove `std::cout` from the list of streams, and add a log file instead: ```cpp #include "BS_thread_pool.hpp" // BS::synced_stream, BS::thread_pool @@ -1178,12 +1221,6 @@ It is common practice to create a global `BS::synced_stream` object, so that it Internally, `BS::synced_stream` keeps the streams in an `std::vector`. The order in which the streams are added is also the order in which they will be printed to. For more precise control, you can use the member function `get_streams()` to get a reference to this vector, and manipulate it directly as you see fit. -### Synchronizing tasks with `BS::counting_semaphore` and `BS::binary_semaphore` - -The thread pool library provides two utility classes, `BS::counting_semaphore` and `BS::binary_semaphore`, which offer versatile synchronization primitives that can be used to synchronize tasks in a variety of ways. These classes are equivalent to the C++20 `std::counting_semaphore` and `std::binary_semaphore`, respectively, but are offered in the library as convenience polyfills for projects based on C++17. If C++20 features are available, the polyfills are not used, and instead are just aliases for the standard library classes. - -Since `BS::counting_semaphore` and `BS::binary_semaphore` are identical in functionality to their standard library counterparts, we will not explain how to use them here. Instead, the user is referred to [cppreference.com](https://en.cppreference.com/w/cpp/thread/counting_semaphore). - ## Managing tasks ### Monitoring the tasks @@ -1285,7 +1322,7 @@ int main() } ``` -The program submit 8 tasks to the queue. Each task waits 100 milliseconds and then prints a message. The thread pool has 4 threads, so it will execute the first 4 tasks in parallel, and then the remaining 4. We wait 50 milliseconds, to ensure that the first 4 tasks have all started running. Then we call `purge()` to purge the remaining 4 tasks. As a result, these tasks never get executed. However, since the first 4 tasks are still running when `purge()` is called, they will finish uninterrupted; `purge()` only discards tasks that have not yet started running. The output of the program therefore only contains the messages from the first 4 tasks: +The program submits 8 tasks to the queue. Each task waits 100 milliseconds and then prints a message. The thread pool has 4 threads, so it will execute the first 4 tasks in parallel, and then the remaining 4. We wait 50 milliseconds, to ensure that the first 4 tasks have all started running. Then we call `purge()` to purge the remaining 4 tasks. As a result, these tasks never get executed. However, since the first 4 tasks are still running when `purge()` is called, they will finish uninterrupted; `purge()` only discards tasks that have not yet started running. The output of the program therefore only contains the messages from the first 4 tasks: ```none Task 0 done. @@ -1591,7 +1628,7 @@ In this example, we create a `thread_local` Mersenne twister engine, meaning tha Note that the lambda function we passed to `submit_sequence()` has the signature `[](int)`, with an unnamed `int` argument, as it does not make use of the sequence index, which will be a number in the range `[0, 4)`. This is an easy way to simply submit the same task multiple times. -**Warning:** Exceptions thrown by thread initialization functions must not throw any exceptions, as that will result in program termination. Any exceptions must be handled explicitly within the function. +**Warning:** Thread initialization functions must not throw any exceptions, as that will result in program termination. Any exceptions must be handled explicitly within the function. ### Thread cleanup functions @@ -1635,7 +1672,7 @@ In this example, we create 4 threads, each of which has a separate thread-local We submit 40 tasks to the queue using `submit_sequence()`, each of which prints a message to the log file indicating which thread it is running on. When the `main()` function exits and `pool` is destroyed, the cleanup function is called for each thread, ensuring that the log files are closed properly. -**Warning:** As with initialization functions, exceptions thrown by thread cleanup functions must not throw any exceptions, as that will result in program termination. Any exceptions must be handled explicitly within the function. +**Warning:** As with initialization functions, thread cleanup functions must not throw any exceptions, as that will result in program termination. Any exceptions must be handled explicitly within the function. ### Passing task arguments by constant reference @@ -1686,7 +1723,7 @@ Generally, it is not really necessary to pass arguments by constant reference, b ### Enabling features -The thread pool has some optional features, which are disabled by default to minimize overhead. They can be enabled by passing the appropriate template parameter to the `BS::thread_pool` class when creating the pool. The template parameter is a bitmask, so you can enable several features at once by combining them with the bitwise OR operator `|`. The bitmask flags are members of the `BS::tp` enumeration: +The thread pool has some optional features, which are disabled by default to minimize overhead. They can be enabled by passing the appropriate template parameter to the `BS::thread_pool` class when creating the pool. The template parameter is a bitmask, so you can enable several features at once by combining them with the bitwise OR operator `|`. The bitmask flags are members of the `BS::tp` enumeration class: * `BS::tp::priority` enables [task priority](#setting-task-priority). * `BS::tp::pause` enables [pausing the pool](#pausing-the-pool). @@ -1714,7 +1751,7 @@ Note that, since optional features are enabled separately for each `BS::thread_p Turning on the `BS::tp::priority` flag in the template parameter to `BS::thread_pool` enables task priority. In addition, the library defines the convenience alias `BS::priority_thread_pool`, which is equivalent to `BS::thread_pool`. When this feature is enabled, the static member `priority_enabled` will be set to `true`. -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 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()`, `submit_sequence()`, `detach_bulk()`, and `submit_bulk()`. 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 8-bit integer, so it can have any value between -128 and +127. 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. @@ -1777,7 +1814,7 @@ Lastly, please note that when using the priority queue, tasks will not necessari Turning on the `BS::tp::pause` flag in the template parameter to `BS::thread_pool` enables pausing the pool. In addition, the library defines the convenience alias `BS::pause_thread_pool`, which is equivalent to `BS::thread_pool`. When this feature is enabled, the static member `pause_enabled` will be set to `true`. -This feature enables 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()`. +This feature enables 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 executing 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: @@ -2000,7 +2037,7 @@ Wait deadlock checks are disabled by default because wait deadlocks are not some ### Enabling the native extensions -While portability is one of the guiding principle for developing this library, non-portable features such as setting the thread priority using the operating system's native API can be very useful. Therefore, the library includes native extensions - which are disabled by default, as they are not portable. +While portability is one of the guiding principle for developing this library, non-portable features such as setting the thread priority using the operating system's native API can be very useful. Therefore, the library includes native extensions - which are disabled by default, as they are not portable. (Note that as long as the native extensions are disabled, the library is 100% standard C++.) The native extensions may be enabled by defining the macro `BS_THREAD_POOL_NATIVE_EXTENSIONS` at compilation time. If including the library as a header file, the macro must be defined before `#include "BS_thread_pool.hpp"`. Note that even if the macro is defined, the native extensions are disabled automatically if a supported operating system (Windows, Linux, or macOS) is not detected. @@ -2010,7 +2047,7 @@ If importing the library [as a C++20 module](#importing-the-library-as The `constexpr` flag `BS::thread_pool_native_extensions` indicates whether the thread pool library was compiled with native extensions enabled. Note that the flag will be `false` if `BS_THREAD_POOL_NATIVE_EXTENSIONS` is defined but the operating system is unsupported. -**Warning:** Please note that, as of v5.0.0 of the thread pool library, the native extensions have only been tested on **Windows 11 23H2, Ubuntu 24.10, and macOS 15.1**. They have not been tested on older versions of these operating systems, other Linux distributions, or any other operating systems, and are therefore not guaranteed to work on every system. If you encounter any issues, please report them on [the GitHub repository](https://github.com/bshoshany/thread-pool). +**Warning:** Please note that the native extensions have only been tested on the operating systems listed above under [compiling and compatibility](#compiling-and-compatibility). They have not been tested on older versions of these operating systems, other Linux distributions, or any other operating systems, and are therefore not guaranteed to work on every system. If you encounter any issues, please report them on [the GitHub repository](https://github.com/bshoshany/thread-pool). ### Setting thread priority @@ -2085,12 +2122,12 @@ The thread pool's native extensions allow the user to set a thread's processor a This can be useful for performance optimization, as it can reduce cache misses. However, it can also degrade performance, sometimes severely, since the thread will not run at all until its assigned cores are available. Therefore, it is usually better to let the operating system's scheduler manage thread affinities on its own, except in very specific cases. -Please note that setting thread affinity works on Windows and Linux, but not on macOS, as the native API does not allow it. As affinity is handled differently on different operating systems, the thread pool library provides an abstraction layer over the native APIs. In this abstraction layer, affinity is controlled using an `std::vector` where each element corresponds to a logical processor. +Please note that setting thread affinity works on Windows and Linux, but not on macOS and Android, as the native API does not allow it. As affinity is handled differently on different operating systems, the thread pool library provides an abstraction layer over the native APIs. In this abstraction layer, affinity is controlled using an `std::vector` where each element corresponds to a logical processor. Thread affinity is managed using two static member functions of the `BS::this_thread` class: -* `BS::this_thread::get_os_thread_affinity()` gets the current thread's affinity. It returns an object of type `std::optional>`. If the returned object does not contain a value, then the affinity could not be determined. On macOS, this function always returns `std::nullopt`. -* `BS::this_thread::set_os_thread_affinity()` sets the current thread's affinity. It returns `true` if the affinity was set successfully, or `false` otherwise. On macOS, this function always returns `false`. +* `BS::this_thread::get_os_thread_affinity()` gets the current thread's affinity. It returns an object of type `std::optional>`. If the returned object does not contain a value, then the affinity could not be determined. On macOS and Android, this function always returns `std::nullopt`. +* `BS::this_thread::set_os_thread_affinity()` sets the current thread's affinity. It returns `true` if the affinity was set successfully, or `false` otherwise. On macOS and Android, this function always returns `false`. Note that the thread affinity must be a subset of the process affinity (as obtained using [`BS::get_os_process_affinity()`](#setting-process-affinity)) for the containing process of a thread. @@ -2258,8 +2295,51 @@ The thread pool's native extensions also allow the user to set the entire proces Process affinity is managed using two functions: -* `BS::this_thread::get_os_process_affinity()` gets the process's affinity. It returns an object of type `std::optional>`. If the returned object does not contain a value, then the affinity could not be determined. On macOS, this function always returns `std::nullopt`. -* `BS::this_thread::set_os_process_affinity()` sets the process's affinity. It returns `true` if the affinity was set successfully, or `false` otherwise. On macOS, this function always returns `false`. +* `BS::get_os_process_affinity()` gets the process's affinity. It returns an object of type `std::optional>`. If the returned object does not contain a value, then the affinity could not be determined. On macOS, this function always returns `std::nullopt`. +* `BS::set_os_process_affinity()` sets the process's affinity. It returns `true` if the affinity was set successfully, or `false` otherwise. On macOS, this function always returns `false`. + +Note that by counting the number of elements set to `true` in `BS::get_os_process_affinity()`, you can determine how many logical processors are available to the process. If the native extensions are enabled, a pool created with the default constructor will use this method to determine the number of threads available to the process, which can be less than the number of hardware threads, and use this as the default number of pool threads. This is demonstrated by the following program: + +```cpp +#define BS_THREAD_POOL_NATIVE_EXTENSIONS +#include "BS_thread_pool.hpp" // BS::get_os_process_affinity(), BS::set_os_process_affinity, BS::synced_stream, BS::thread_pool +#include // std::count +#include // std::optional +#include // std::thread +#include // std::vector + +BS::synced_stream sync_out; + +int main() +{ + sync_out.println("Total hardware threads: ", std::thread::hardware_concurrency()); + BS::thread_pool pool1; + sync_out.println("Threads in first pool: ", pool1.get_thread_count()); + + const bool success = BS::set_os_process_affinity({true, true, true}); + if (success) + { + const std::optional> affinity = BS::get_os_process_affinity(); + if (affinity) + { + sync_out.println("Total threads now available to the process: ", std::count(affinity->begin(), affinity->end(), true)); + BS::thread_pool pool2; + sync_out.println("Threads in second pool: ", pool2.get_thread_count()); + return 0; + } + } + sync_out.println("ERROR: Failed to set or get process affinity."); +} +``` + +Assuming that the program was executed without setting the affinity of the process beforehand (e.g. using `taskset` on Linux), `pool1` will be created with the total number of hardware threads. However, we then manually set the affinity of the process so that only the first 3 logical processors are enabled (by passing a vector with 3 `true` elements and all other elements `false`). Therefore, `pool2` will be created with only 3 threads. If, for example, 32 hardware threads are available in total, the output will be: + +```none +Total hardware threads: 32 +Threads in first pool: 32 +Total threads now available to the process: 3 +Threads in second pool: 3 +``` ### Accessing native thread handles @@ -2308,24 +2388,26 @@ The test program also takes the following command line arguments: * `log`: Print to a log file. It will have the same name as the executable, with a suffix `-yyyy-mm-dd_hh.mm.ss.log` based on the current date and time. * `tests`: Perform standard tests. * `deadlock`: Perform long deadlock tests. -* `benchmarks`: Perform full Mandelbrot plot benchmarks. -* `plot`: Perform quick Mandelbrot plot benchmarks. -* `save`: Save the Mandelbrot plot to a file. +* `benchmarks`: Perform full Mandelbrot set benchmarks. +* `plot`: Perform quick Mandelbrot set benchmarks. +* `save`: Save the Mandelbrot set image to a file. -If no options are entered, the default is `benchmarks log stdout tests`. If the file `default_args.txt` exists in the same folder, the test program reads the default arguments from it (space separated in a single line). Command line arguments can still override these defaults. This is useful when debugging. +If no options are entered, the default is `benchmarks log stdout tests`. If the file `default_args.txt` exists in the same folder or the parent folder, the test program reads the default arguments from it (space separated in a single line). Command line arguments can still override these defaults. This is useful when debugging. The following macros can be defined during compilation (using the `-D` flag in Clang and GCC or `/D` in MSVC) to enable additional features: * `BS_THREAD_POOL_TEST_IMPORT_MODULE`: Import the thread pool library [as a C++20 module](#importing-the-library-as-a-c20-module). Note that the module must be compiled beforehand, as explained in the relevant section. * `BS_THREAD_POOL_NATIVE_EXTENSIONS`: Test the [native extensions](#native-extensions). If importing the library as a C++20 module, ensure that the library was compiled with the same macro. -A Python script, `test_all.py`, is provided for convenience in the `scripts` folder. This script makes use of the bundled [`compile_cpp.py` script](#the-compile_cpppy-script), and requires Python 3.12 or later. The script will automatically detect if Clang, GCC, and/or MSVC are available, and compile and run the test program using each available compiler 3 times: +The bundled [`compile_cpp.py` script](#the-compile_cpppy-script), if run with `python scripts/compile_cpp.py tests/BS_thread_pool_test.cpp --run --try-all --type=release --verbose`, will automatically detect if Clang, GCC, and/or MSVC are available, and compile and run the test program using each available compiler 3 times: 1. With C++17 support. 2. With C++20 support, using `import BS.thread_pool`. -3. With C++23 support, using `import BS.thread_pool`, and using `import std` on supported compilers. +3. With C++23 support, using `import BS.thread_pool`, and using `import std`. -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. However, please note that only the latest versions of each compiler are supported. +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. However, please note that **only the latest versions of each compiler are supported**. + +By default, the test program prints colored output using ANSI escape codes for better readability. This can be disabled by setting the `NO_COLOR` environment variable. ### Performance tests @@ -2335,7 +2417,9 @@ These benchmarks are heavily CPU-intensive, which results in a high speedup fact The full benchmarks are enabled using the command line argument `benchmarks`, which is enabled by default. The command line argument `plot` can be used to just plot the Mandelbrot set once, either instead of or in addition to doing the full benchmarks. This will plot the largest possible image that can be plotted in 5 seconds, and only measure the performance in pixels/ms for the entire plot. -If you want to see the actual plot, pass the `save` command line argument. The plot is saved to a BMP file, to avoid having to depend on 3rd-party libraries. This is off by default, since that file can get quite large. +The test program prints out the Mandelbrot set it generates, downsampled to fit in a terminal window (at 120 character width). This will be in 24-bit color in the terminal, and in monochrome using Unicode blocks in the log file. If the `NO_COLOR` environment variable is set, the terminal output will also be in monochrome. (Note: On Windows Terminal, ensure that `adjustIndistinguishableColors` is disabled in the settings, otherwise the plot will not be displayed correctly.) + +If you want to see the plot in full resolution, pass the `save` command line argument, and the plot will be saved to `BS_thread_pool_benchmark_mandelbrot.bmp` (it's a BMP file to avoid having to depend on 3rd-party libraries). This is off by default, since that file can get quite large. The program determines the optimal resolution of the Mandelbrot plot by testing how many pixels are needed to reach a certain target duration when parallelizing the loop using a number of tasks equal to the number of threads. This ensures that the benchmarks take approximately the same amount of time (per thread) on all systems, and are thus more consistent and portable. @@ -2348,40 +2432,40 @@ If the [native extensions](#native-extensions) are enabled, the program will try As an example, here are the results of the benchmarks running on a 24-core (8P+16E) / 32-thread Intel i9-13900K CPU. The tests were compiled using MSVC in C++23 mode, to obtain maximum performance using the latest C++23 features. Compiler optimizations were enabled using the `/O2` flag. The benchmarks were run 5 times, and the result with the median speedup was as follows: ```none -Generating a 3965x3965 plot of the Mandelbrot set... +Generating a 3927x3927 plot of the Mandelbrot set... Each test will be repeated 30 times to collect reliable statistics. 1 task: [..............................] (single-threaded) --> Mean: 510.5 ms, standard deviation: 0.5 ms, speed: 1026.5 pixels/ms. +-> Mean: 500.8 ms, standard deviation: 1.3 ms, speed: 1026.5 pixels/ms. 8 tasks: [..............................] --> Mean: 149.1 ms, standard deviation: 0.6 ms, speed: 3514.7 pixels/ms. +-> Mean: 146.0 ms, standard deviation: 0.3 ms, speed: 3520.9 pixels/ms. 16 tasks: [..............................] --> Mean: 85.4 ms, standard deviation: 2.5 ms, speed: 6133.9 pixels/ms. +-> Mean: 82.2 ms, standard deviation: 1.5 ms, speed: 6256.1 pixels/ms. 32 tasks: [..............................] --> Mean: 48.3 ms, standard deviation: 1.8 ms, speed: 10849.7 pixels/ms. +-> Mean: 49.8 ms, standard deviation: 1.2 ms, speed: 10322.2 pixels/ms. 64 tasks: [..............................] --> Mean: 29.1 ms, standard deviation: 1.0 ms, speed: 17987.7 pixels/ms. +-> Mean: 26.9 ms, standard deviation: 1.2 ms, speed: 19109.5 pixels/ms. 128 tasks: [..............................] --> Mean: 23.6 ms, standard deviation: 0.7 ms, speed: 22173.8 pixels/ms. +-> Mean: 22.8 ms, standard deviation: 0.9 ms, speed: 22545.8 pixels/ms. 256 tasks: [..............................] --> Mean: 22.5 ms, standard deviation: 0.6 ms, speed: 23325.3 pixels/ms. +-> Mean: 21.4 ms, standard deviation: 0.5 ms, speed: 24058.2 pixels/ms. 512 tasks: [..............................] --> Mean: 21.8 ms, standard deviation: 0.5 ms, speed: 24075.4 pixels/ms. +-> Mean: 20.7 ms, standard deviation: 0.6 ms, speed: 24833.1 pixels/ms. 1024 tasks: [..............................] --> Mean: 21.9 ms, standard deviation: 0.7 ms, speed: 23892.4 pixels/ms. -Maximum speedup obtained by multithreading vs. single-threading: 23.5x, using 512 tasks. +-> Mean: 21.0 ms, standard deviation: 0.4 ms, speed: 24478.3 pixels/ms. +Maximum speedup obtained by multithreading vs. single-threading: 24.2x, using 512 tasks. ``` This CPU has 24 cores, of which 8 are fast (5.40 GHz max) performance cores with hyperthreading (thus providing 16 threads in total), and 16 are slower (4.30 GHz max) efficiency cores without hyperthreading, for a total of 32 threads. Due to the hybrid architecture, it is not trivial to calculate the theoretical maximum speedup. However, we can get a rough estimate by noticing that the E-cores are about 20% slower than the P-cores, and that hyperthreading is generally known to provide around a 30% speedup. Thus, the estimated theoretical speedup (compared to a single P-core) is 8 × 1.3 + 16 × 0.8 = 23.2x. -The actual median speedup obtained, 23.5x, is slightly above this estimate, which indicates that the thread pool provides optimal performance and allows the Mandelbrot plot algorithm to take full advantage of the CPU's capabilities. +The actual median speedup obtained, 24.2x, is 4.3% above this estimate, which indicates that the thread pool provides optimal performance and allows the Mandelbrot plot algorithm to take full advantage of the CPU's capabilities. It should also be noted that even though the available number of hardware threads is 32, the maximum possible speedup is achieved not with 32 tasks, but with 512 tasks - half the square of the number of hardware threads. The reason for this is that splitting the job into more tasks than threads eliminates thread idle time, as explained [above](#optimizing-the-number-of-blocks). However, at 1024 tasks we encounter diminishing returns, as the overhead of submitting the tasks to the pool starts to outweigh the benefits of parallelization. ### Finding the version of the library -Starting with v5.0.0, the thread pool library defines the `constexpr` object `BS::thread_pool_version`, which can be used to check the version of the library at compilation time. This object is of type `BS::version`, with members `major`, `minor`, and `patch`, and all comparison operators defined as `constexpr`. It also has a `to_string()` member function and an `operator<<` overload for easy printing at runtime. +Starting with v5.0.0, the thread pool library defines the `constexpr` object `BS::thread_pool_version`, which can be used to check the version of the library at compilation time. This object is of type `BS::version`, with members `major`, `minor`, and `patch`, and all comparison operators defined as `constexpr`. It also has a `to_string()` member function and an `operator<<` overload for easy printing at runtime (used by the [test program](#testing-the-library)). Since `BS::thread_pool_version` is a `constexpr` object, it can be used in any context where a `constexpr` object is allowed, such as `static_assert()` and `if constexpr`. For example, the following program will fail to compile if the version is not 5.1.0 or higher: @@ -2418,9 +2502,7 @@ int main() } ``` -Currently, both the examples above are of pedagogical value only, because `BS::thread_pool_version` was only introduced in v5.0.0, and that is also the latest version at the time of writing, so there are no other versions to compare to. However, once future versions of the library are released, this object will be the preferred way to do version checking. - -For backwards compatibility, if you are not sure if you are going to get v4 or v5 of the library, you can check the version using the following preprocessor macros, which were introduced in v4.0.1: +`BS::thread_pool_version` was introduced in v5.0.0, and it is the preferred way to do version checking. However, for backwards compatibility, if you are not sure if you are going to get v4 or v5 of the library, you can check the version using the following preprocessor macros, which were introduced in v4.0.1: * `BS_THREAD_POOL_VERSION_MAJOR` - indicates the major version. * `BS_THREAD_POOL_VERSION_MINOR` - indicates the minor version. @@ -2495,7 +2577,7 @@ Below we will provide the commands for compiling the library as a module and the │ └── BS_thread_pool.hpp <- the header file ├── modules │ └── BS.thread_pool.cppm <- the module file -├── tasks +├── scripts │ └── compile_cpp.py <- the compile script (optional) └── tests └── BS_thread_pool_test.cpp <- the test program @@ -2519,11 +2601,11 @@ Since we used `-t=release`, optimization flags will be added automatically. If y Thread pool library imported using: import BS.thread_pool (C++20 modules). ``` -For further customization, it is recommend to create a `compile_cpp.yaml` file as explained [below](#the-compile_cpppy-script). +For further customization, it is recommended to create a `compile_cpp.yaml` file as explained [below](#the-compile_cpppy-script). ### Compiling with Clang using `import BS.thread_pool` -Note: The following instructions have only been tested using Clang v19.1.6, the latest version at the time of writing, and may not work with older versions of the compiler. +Note: The following instructions have only been tested using Clang v21.1.8, the latest version at the time of writing, and may not work with older versions of the compiler. To compile the module file `BS.thread_pool.cppm` with Clang, first create the `build` folder using `mkdir build`, and then run the following command in the root folder of the repository: @@ -2563,16 +2645,18 @@ Thread pool library imported using: import BS.thread_pool (C++20 modules). Of course, you should add warning, debugging, optimization, and other compiler flags to the commands above as needed. For more information about using C++20 modules with Clang, please see [the official documentation](https://clang.llvm.org/docs/StandardCPlusPlusModules.html). -**Note:** On macOS, Apple Clang v16.0.0 (the latest version at the time of writing) does not support C++20 modules. Please either install the latest version of LLVM Clang using [Homebrew](https://formulae.brew.sh/formula/llvm), or include the library as a header file. +**Note:** At the time of writing, there is a bug in Clang with libc++ where using `std::jthread` in a C++20 module causes a compilation error. As a workaround, until the bug is fixed, the thread pool library automatically falls back to `std::thread` if it detects that Clang and libc++ are being used together with C++20 modules. This workaround can be disabled by defining `BS_THREAD_POOL_DISABLE_WORKAROUNDS` when compiling the module. + +**Note:** On macOS, Apple Clang does not support C++20 modules. Please either install the latest version of LLVM Clang using [Homebrew](https://formulae.brew.sh/formula/llvm), or include the library as a header file. ### Compiling with GCC using `import BS.thread_pool` -Note: The following instructions have only been tested using GCC v14.2.0, the latest version at the time of writing, and may not work with older versions of the compiler. +Note: The following instructions have only been tested using GCC v15.2.0, the latest version at the time of writing, and may not work with older versions of the compiler. To compile the module file `BS.thread_pool.cppm` with GCC, first create the `build` folder using `mkdir build`, and then run the following command in the root folder of the repository: ```bash -g++ -x c++ modules/BS.thread_pool.cppm -c "-fmodule-mapper=|@g++-mapper-server -r build" -fmodule-only -fmodules-ts -std=c++20 -I include +g++ -x c++ modules/BS.thread_pool.cppm -c "-fmodule-mapper=|@g++-mapper-server -r build" -fmodule-only -fmodules -std=c++20 -I include ``` Here is a breakdown of the compiler arguments: @@ -2582,7 +2666,7 @@ Here is a breakdown of the compiler arguments: * `-c`: Do not run the linker, only compile the module. * `"-fmodule-mapper=|@g++-mapper-server -r build"`: Specify to the module mapper that the compiled module should be placed in the `build` folder. This will create a file `build/BS.thread_pool.gcm`. The extension `.gcm` is used by GCC for compiled modules. * `-fmodule-only`: Do not create an object file for the module. -* `-fmodules-ts`: Enable C++20 modules. +* `-fmodules`: Enable C++20 modules. * `-std=c++20`: Use the C++20 standard. For C++23, use `-std=c++23`. * `-I include`: Add the `include` folder to the include path, so that the module can find the header file `BS_thread_pool.hpp`. @@ -2591,14 +2675,14 @@ Note that to enable the [native extensions](#native-extensions), you should add Once the module is compiled, you can compile the test program as follows: ```bash -g++ tests/BS_thread_pool_test.cpp "-fmodule-mapper=|@g++-mapper-server -r build" -fmodules-ts -std=c++20 -o build/BS_thread_pool_test -D BS_THREAD_POOL_TEST_IMPORT_MODULE +g++ tests/BS_thread_pool_test.cpp "-fmodule-mapper=|@g++-mapper-server -r build" -fmodules -std=c++20 -o build/BS_thread_pool_test -D BS_THREAD_POOL_TEST_IMPORT_MODULE ``` Here is a breakdown of the compiler arguments: * `tests/BS_thread_pool_test.cpp`: The program to compile. * `"-fmodule-mapper=|@g++-mapper-server -r build"`: Specify to the module mapper that the compiled module can be found in the `build` folder. It will look for the file `build/BS.thread_pool.gcm`. -* `-fmodules-ts`, `-std=c++20`: Same as above. +* `-fmodules`, `-std=c++20`: Same as above. * `-o build/BS_thread_pool_test`: Output the compiled program to `build/BS_thread_pool_test` (or `build/BS_thread_pool_test.exe` on Windows). * `-D BS_THREAD_POOL_TEST_IMPORT_MODULE`: Define the macro `BS_THREAD_POOL_TEST_IMPORT_MODULE`, which is used to indicate to the test program that it needs to import the library as a module instead of including the header file. **Note that this macro is only used by the test program; it is not needed when you compile your own programs.** @@ -2610,21 +2694,19 @@ Thread pool library imported using: import BS.thread_pool (C++20 modules). Of course, you should add warning, debugging, optimization, and other compiler flags to the commands above as needed. For more information about using C++20 modules with GCC, please see [the official documentation](https://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Modules.html). -**Note:** GCC v14.2.0 (latest version at the time of writing) appears to have an internal compiler error when compiling programs containing modules (or at least, this particular module) with any optimization flags other than `-Og` enabled. Until this is fixed, if you wish to use compiler optimizations, please either include the library as a header file or use a different compiler. - ### Compiling with MSVC using `import BS.thread_pool` -Note: The following instructions have only been tested using MSVC v19.42.34435, the latest version at the time of writing, and may not work with older versions of the compiler. +Note: The following instructions have only been tested using MSVC v19.50.35721, the latest version at the time of writing, and may not work with older versions of the compiler. -To compile the module file `BS.thread_pool.cppm` with MSVC, first open the Visual Studio Developer PowerShell for the appropriate CPU architecture. For example, for x64, execute the following command in PowerShell: +To compile the module file `BS.thread_pool.cppm` with MSVC, first open the Visual Studio Developer PowerShell for the appropriate CPU architecture. For example, on Visual Studio 2026, for x64 architecture, execute the following command in PowerShell in the root folder of the repository: ```pwsh -& 'C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\Launch-VsDevShell.ps1' -Arch amd64 -HostArch amd64 +& 'C:\Program Files\Microsoft Visual Studio\18\Community\Common7\Tools\Launch-VsDevShell.ps1' -Arch amd64 -HostArch amd64 -SkipAutomaticLocation ``` -For ARM64, replace `amd64` with `arm64`. (Do not use the "Developer PowerShell for VS 2022" Start Menu shortcut, as it may not use the correct CPU architecture by default.) +For ARM64, replace `amd64` with `arm64`. (Do not use the "Developer PowerShell for VS" Start Menu shortcut, as it may not use the correct CPU architecture by default.) -Navigate to the repository folder, create the `build` folder using `mkdir build`, and then run the following command in the root folder of the repository: +Create the `build` folder using `mkdir build`, and then run the following command in the root folder of the repository: ```pwsh cl modules/BS.thread_pool.cppm /c /EHsc /interface /nologo /permissive- /std:c++20 /TP /Zc:__cplusplus /I include /ifcOutput build/BS.thread_pool.ifc /Fo:build/BS.thread_pool.obj @@ -2673,12 +2755,12 @@ Of course, you should add warning, debugging, optimization, and other compiler f ### Compiling with CMake using `import BS.thread_pool` -Note: The following instructions have only been tested using CMake v3.31.2, the latest version at the time of writing, and may not work with older versions. Also, modules are currently only supported by CMake with the [`Ninja`](https://ninja-build.org/) and `Visual Studio 17 2022` generators. +Note: The following instructions have only been tested using CMake v4.2.1, the latest version at the time of writing, and may not work with older versions. Also, modules are currently not supported by CMake with all generators; please see the CMake documentation for more information. If you are using [CMake](https://cmake.org/), you can use `target_sources()` with `CXX_MODULES` to include the module file `BS.thread_pool.cppm`. CMake will then automatically compile the module and link it to your program. Here is an example of a `CMakeLists.txt` file that can be used to build the test program and import the thread pool library as a module: ```cmake -cmake_minimum_required(VERSION 3.31) +cmake_minimum_required(VERSION 4.2.1) project(BS_thread_pool_test LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -2727,10 +2809,9 @@ If C++23 features are available, the thread pool library can import th At the time of writing, importing the C++ Standard Library as a module is only officially supported by the following combinations of compilers and standard libraries: -* Recent versions of MSVC with Microsoft STL. * Recent versions of LLVM Clang (**not** Apple Clang) with LLVM libc++. - -It is not supported by GCC with any standard library, Clang with any standard library other than libc++, any compiler with GNU libstdc++, or any other compiler or standard library. +* Recent versions of GCC with libstdc++. +* Recent versions of MSVC with Microsoft STL. If `BS_THREAD_POOL_IMPORT_STD` is defined, then you must also import the thread pool library itself as a module. If the library is included as a header file, this will force the program that included the header file to also import `std`, which is not desirable and can lead to compilation errors if the program `#include`s any Standard Library header files. @@ -2738,7 +2819,7 @@ Defining the macro before importing the module will not work, as modules cannot [The test program](#testing-the-library) will also import the `std` module if the macro `BS_THREAD_POOL_IMPORT_STD` is defined at compilation time. In that case, you should also enable the macro `BS_THREAD_POOL_TEST_IMPORT_MODULE` to import the thread pool library as a module. -The `constexpr` flag `BS::thread_pool_import_std` indicates whether the thread pool library was compiled with `import std`. Note that the flag will be `false` if `BS_THREAD_POOL_IMPORT_STD` is defined but the compiler or standard library does not support importing the C++ Standard Library as a module. +The `constexpr` flag `BS::thread_pool_import_std` indicates whether the thread pool library was compiled with `import std`. Note that the flag will be `false` if `BS_THREAD_POOL_IMPORT_STD` is defined but C++23 support is not enabled in the compiler. At the time of writing, importing the `std` module requires compiling it first. As explained in the [previous section](#importing-the-library-as-a-c20-module), using the bundled `compile_cpp.py` script is the easiest way to do this, as we show in the [next section](#compiling-with-compile_cpppy-using-import-std). However, for those who wish to compile manually, in the following sections we will explain how to do it with both Clang and MSVC, as well as with CMake. It is assumed that the reader has already read the section about importing the `BS.thread_pool` library as a module, so we omit some details here. @@ -2764,17 +2845,17 @@ C++ Standard Library imported using: * Test program: import std (C++23 std module). ``` -For further customization, it is recommend to create a `compile_cpp.yaml` file as explained [below](#the-compile_cpppy-script). +For further customization, it is recommended to create a `compile_cpp.yaml` file as explained [below](#the-compile_cpppy-script). ### Compiling with Clang and LLVM libc++ using `import std` -Note: The following instructions have only been tested using Clang v19.1.6 and LLVM libc++ v19.1.6, the latest versions at the time of writing, and may not work with older versions. +Note: The following instructions have only been tested using Clang v21.1.8 and LLVM libc++ v21.1.8, the latest versions at the time of writing, and may not work with older versions. Before compiling the `std` module, you must find the file `std.cppm`: -* On Windows, libc++ is most likely installed via [MSYS2](https://www.msys2.org/), so the `std` module should be at `C:\msys64\clang64\share\libc++\v1\std.cppm`. If you did not install MSYS2 in `C:\msys64`, replace that with the correct path. If you installed libc++ without MSYS2, locate `std.cppm` manually in the installation folder. -* On Linux, the `std` module should be at `/usr/lib/llvm-/share/libc++/v1/std.cppm`. Replace `` with the major version number of libc++, e.g. `19`. If you installed libc++ in a different folder, locate `std.cppm` manually in that folder. -* On macOS, the `std` module should be at `/usr/local/Cellar/llvm//share/libc++/v1/std.cppm`. Replace `` with the full version number of libc++, e.g. `19.1.6`. If you installed libc++ in a different folder, locate `std.cppm` manually in that folder. +* On Windows, libc++ is most likely installed via [MSYS2](https://www.msys2.org/), so the `std` module should be at `C:\msys64\clang64\share\libc++\v1\std.cppm`. If you did not install MSYS2 in `C:\msys64`, replace that with the correct path. If you installed libc++ without MSYS2, locate `std.cppm` manually in the installation folder. +* On Linux, the `std` module should be at `/usr/lib/llvm-/share/libc++/v1/std.cppm`. Replace `` with the major version number of libc++. If you installed libc++ in a different folder, locate `std.cppm` manually in that folder. +* On macOS with the ([Homebrew build](https://formulae.brew.sh/formula/llvm)), the `std` module should be at `/usr/local/Cellar/llvm//share/libc++/v1/std.cppm`. Replace `` with the full version number of libc++. If you installed libc++ in a different folder, locate `std.cppm` manually in that folder. To compile the module file `std.cppm` with Clang, first create the `build` folder using `mkdir build`, and then run the following command in the root folder of the repository: @@ -2784,7 +2865,7 @@ clang++ "path to std.cppm" --precompile -std=c++23 -o build/std.pcm -Wno-reserve Of course, you should replace `"path to std.cppm"` with the actual path. The compiler arguments are explained [above](#compiling-with-clang-using-import-bsthread_pool). The additional argument `-Wno-reserved-module-identifier` is needed to silence a false-positive warning. -Next, compile the `BS.thread_pool` module as [above](#compiling-with-clang-using-import-bsthread_pool), but with the following additional flags: +Next, compile the `BS.thread_pool` module as [above](#compiling-with-clang-using-import-bsthread_pool), but with `-std=c++23` and the following additional flags: * `-fmodule-file="std=build/std.pcm"`: Specify that the module `std` is located in the file `build/std.pcm`. * `-D BS_THREAD_POOL_IMPORT_STD`: Instruct the library to import the `std` module. @@ -2807,11 +2888,47 @@ C++ Standard Library imported using: * Test program: import std (C++23 std module). ``` +### Compiling with GCC and GNU libstdc++ using `import std` + +Note: The following instructions have only been tested using GCC v15.2.0 and GNU libstdc++ v15 (20250917), the latest versions at the time of writing, and may not work with older versions. + +With GNU libstdc++, the `std` module file is always available as the system module `bits/std.cc`. To compile this module file with GCC, first create the `build` folder using `mkdir build`, and then run the following command in the root folder of the repository: + +```bash +g++ -fsearch-include-path bits/std.cc -c "-fmodule-mapper=|@g++-mapper-server -r build" -fmodule-only -fmodules -std=c++23 -I include +``` + +The compiler arguments are explained [above](#compiling-with-gcc-using-import-bsthread_pool). The additional argument `-fsearch-include-path` is needed to tell the compiler to look for `bits/std.cc` in the include path (otherwise it will assume it is in the current directory). + +Next, compile the `BS.thread_pool` module as [above](#compiling-with-gcc-using-import-bsthread_pool), but with `-std=c++23` and the following additional flags: + +* `-D BS_THREAD_POOL_IMPORT_STD`: Instruct the library to import the `std` module. + +```bash +g++ -x c++ modules/BS.thread_pool.cppm -c "-fmodule-mapper=|@g++-mapper-server -r build" -fmodule-only -fmodules -std=c++23 -I include -D BS_THREAD_POOL_IMPORT_STD +``` + +Add `-D BS_THREAD_POOL_NATIVE_EXTENSIONS` if you wish to enable the [native extensions](#native-extensions). Once the module is compiled, you can compile the test program as follows: + +```bash +g++ tests/BS_thread_pool_test.cpp "-fmodule-mapper=|@g++-mapper-server -r build" -fmodules -std=c++23 -o build/BS_thread_pool_test -D BS_THREAD_POOL_TEST_IMPORT_MODULE -D BS_THREAD_POOL_IMPORT_STD +``` + +Again, you should add `-D BS_THREAD_POOL_NATIVE_EXTENSIONS` if you wish to test the native extensions. If you now type `build/BS_thread_pool_test`, the test program will run. If the `std` module was successfully imported, the test program will print the message: + +```none +C++ Standard Library imported using: +* Thread pool library: import std (C++23 std module). +* Test program: import std (C++23 std module). +``` + +**NOTE:** At the time of writing, there is a bug when using GCC with libstdc++ on Windows via MSYS2 where the `BS.thread_pool` module doesn't compile if both native extensions and `import std` are enabled. As a workaround, until the bug is fixed, the thread pool library automatically falls back to header files if it detects that GCC and libstdc++ are being used together with the C++23 `std` module on Windows. This workaround can be disabled by defining `BS_THREAD_POOL_DISABLE_WORKAROUNDS` when compiling the module. + ### Compiling with MSVC and Microsoft STL using `import std` -Note: The following instructions have only been tested using MSVC v19.42.34435 and Microsoft STL v143 (202408), the latest versions at the time of writing, and may not work with older versions. +Note: The following instructions have only been tested using MSVC v19.50.35721 and Microsoft STL v145 (202508), the latest versions at the time of writing, and may not work with older versions. -Before compiling the `std` module, you must find the file `std.ixx`. It should be located in the folder `C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\\modules`. Replace `` with the full version number of the MSVC runtime library, e.g. `14.42.34433`. If you installed Visual Studio in a different folder, locate `std.ixx` manually in that folder. +Before compiling the `std` module, you must find the file `std.ixx`. If you have Visual Studio 2026, it should be located in the folder `C:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\\modules`. Replace `` with the full version number of the MSVC runtime library; the latest is `14.50.35717` at the time of writing. If you installed Visual Studio in a different folder, locate `std.ixx` manually in that folder. To compile the module file `std.ixx` with MSVC, first open the Visual Studio Developer PowerShell for the appropriate CPU architecture as explained [above](#compiling-with-msvc-using-import-bsthread_pool). Navigate to the repository folder, create the `build` folder using `mkdir build`, and then run the following command in the root folder of the repository: @@ -2846,12 +2963,12 @@ C++ Standard Library imported using: ### Compiling with CMake using `import std` -Note: The following instructions have only been tested using CMake v3.31.2, the latest version at the time of writing, and may not work with older versions. Also, modules are currently only supported by CMake with the [`Ninja`](https://ninja-build.org/) and `Visual Studio 17 2022` generators. +Note: The following instructions have only been tested using CMake v4.2.1, the latest version at the time of writing, and may not work with older versions. Also, modules are currently not supported by CMake with all generators; please see the CMake documentation for more information. If you are using [CMake](https://cmake.org/), you can enable `CMAKE_EXPERIMENTAL_CXX_IMPORT_STD` to automatically compile the `std` module, provided the compiler and standard library support it. Here is an example of a `CMakeLists.txt` file that can be used to build the test program, import the thread pool library as a module, and import the C++ Standard Library as a module: ```cmake -cmake_minimum_required(VERSION 3.31) +cmake_minimum_required(VERSION 4.2.1) project(BS_thread_pool_test LANGUAGES CXX) set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -2911,17 +3028,6 @@ vcpkg upgrade Please refer to [this package's page on vcpkg.io](https://vcpkg.io/en/package/bshoshany-thread-pool) for more information. -### 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/5.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: @@ -2936,9 +3042,20 @@ Then, use `dependency('bshoshany-thread-pool')` in your `meson.build` file to in meson wrap update bshoshany-thread-pool ``` +### 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/5.1.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 CMake with CPM -Note: The following instructions have only been tested using CMake v3.31.2 and CPM v0.40.2, the latest versions at the time of writing, and may not work with older versions. +Note: The following instructions have only been tested using CMake v4.2.1 and CPM v0.42.0, the latest versions at the time of writing, and may not work with older versions. If you are using [CMake](https://cmake.org/), you can install `BS::thread_pool` most easily with [CPM](https://github.com/cpm-cmake/CPM.cmake). If CPM is already installed, simply add the following to your project's `CMakeLists.txt`: @@ -2946,7 +3063,7 @@ If you are using [CMake](https://cmake.org/), you can install `BS::thread_pool` CPMAddPackage( NAME BS_thread_pool GITHUB_REPOSITORY bshoshany/thread-pool - VERSION 5.0.0 + VERSION 5.1.0 EXCLUDE_FROM_ALL SYSTEM ) @@ -2959,7 +3076,7 @@ This will automatically download the indicated version of the package from [the A convenient shorthand for GitHub packages also exists, in which case `CPMAddPackage()` can be called with a single argument of the form `"gh:user/name@version"`. After that, `CPM_LAST_PACKAGE_NAME` will be set to the name of the package, so we need to use this variable to define the include folder. This results in a more compact configuration: ```cmake -CPMAddPackage("gh:bshoshany/thread-pool@5.0.0") +CPMAddPackage("gh:bshoshany/thread-pool@5.1.0") add_library(BS_thread_pool INTERFACE) target_include_directories(BS_thread_pool INTERFACE ${${CPM_LAST_PACKAGE_NAME}_SOURCE_DIR}/include) ``` @@ -2974,10 +3091,10 @@ endif() include(${CPM_DOWNLOAD_LOCATION}) ``` -Here is an example of a complete `CMakeLists.txt` which automatically downloads and compiles the test program [`BS_thread_pool_test.cpp`](#automated-tests): +Here is an example of a complete `CMakeLists.txt` which automatically downloads and compiles the test program [`BS_thread_pool_test.cpp`](#testing-the-library): ```cmake -cmake_minimum_required(VERSION 3.31) +cmake_minimum_required(VERSION 4.2.1) project(BS_thread_pool_test LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -2993,7 +3110,7 @@ if(NOT(EXISTS ${CPM_DOWNLOAD_LOCATION})) endif() include(${CPM_DOWNLOAD_LOCATION}) -CPMAddPackage("gh:bshoshany/thread-pool@5.0.0") +CPMAddPackage("gh:bshoshany/thread-pool@5.1.0") add_library(BS_thread_pool INTERFACE) target_include_directories(BS_thread_pool INTERFACE ${${CPM_LAST_PACKAGE_NAME}_SOURCE_DIR}/include) @@ -3001,9 +3118,9 @@ add_executable(${PROJECT_NAME} ${${CPM_LAST_PACKAGE_NAME}_SOURCE_DIR}/tests/BS_t target_link_libraries(${PROJECT_NAME} PRIVATE BS_thread_pool) ``` -Note that for MSVC we have to add the `/permissive-` flag to enforce strict C++ standard conformance, otherwise the test program will not compile, and `/Zc:__cplusplus`, otherwise the test program cannot detect the correct C++ version. This is handled automatically by the `if(MSVC)` block. +The `if(MSVC)` block is explained [above](#compiling-with-msvc-using-import-bsthread_pool). To enable the [native extensions](#native-extensions), add the line `add_compile_definitions(BS_THREAD_POOL_NATIVE_EXTENSIONS)`. -To enable the [native extensions](#native-extensions), add the line `add_compile_definitions(BS_THREAD_POOL_NATIVE_EXTENSIONS)`. Replace `CMAKE_CXX_STANDARD 17` with `20` or `23` if you wish to use C++20 or C++23 features, respectively. Of course, you should add warning, debugging, optimization, and other compiler flags to the configuration above as needed. +Replace `CMAKE_CXX_STANDARD 17` with `20` or `23` if you wish to use C++20 or C++23 features, respectively. Of course, you should add warning, debugging, optimization, and other compiler flags to the configuration above as needed. With this `CMakeLists.txt` in an empty folder, type the following commands to build and run the project: @@ -3017,12 +3134,12 @@ For MSVC, replace the last command with `build/Debug/BS_thread_pool_test`. Pleas ### Installing using CMake with `FetchContent` -Note: The following instructions have only been tested using CMake v3.31.2, the latest version at the time of writing, and may not work with older versions. +Note: The following instructions have only been tested using CMake v4.2.1, the latest version at the time of writing, and may not work with older versions. If you are using [CMake](https://cmake.org/) but do not wish to use 3rd-party tools, you can also install `BS::thread_pool` using the built-in [`FetchContent`](https://cmake.org/cmake/help/latest/module/FetchContent.html) module. Here is an example of a complete `CMakeLists.txt` which automatically downloads and compiles the test program, as in the previous section, but this time using `FetchContent` directly: ```cmake -cmake_minimum_required(VERSION 3.31) +cmake_minimum_required(VERSION 4.2.1) project(BS_thread_pool_test LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -3037,7 +3154,7 @@ set(FETCHCONTENT_UPDATES_DISCONNECTED ON) FetchContent_Declare( bshoshany_thread_pool GIT_REPOSITORY https://github.com/bshoshany/thread-pool.git - GIT_TAG v5.0.0 + GIT_TAG v5.1.0 DOWNLOAD_EXTRACT_TIMESTAMP TRUE EXCLUDE_FROM_ALL SYSTEM @@ -3061,14 +3178,14 @@ Descriptions of each item can also be found in the [Doxygen](https://www.doxygen `BS::thread_pool` is the main thread pool class. It is used to create a pool of threads that continuously execute tasks submitted to a queue. It can take template parameters, which enable optional features as described [below](#optional-features-and-the-template-parameter). The member functions that are available by default, when no template parameters are used, are: * Constructors: - * `thread_pool()`: Construct a new thread pool with a number of threads equal to `std::thread::hardware_concurrency()`. + * `thread_pool()`: Construct a new thread pool with a number of threads equal to `std::thread::hardware_concurrency()`, or if the native extensions are enabled, the number of threads available to the process as obtained from `BS::get_os_process_affinity()`. * `thread_pool(std::size_t num_threads)`: Construct a new thread pool with the specified number of threads. - * `thread_pool(F&& init)`: Construct a new thread pool with a number of threads equal to `std::thread::hardware_concurrency()` and the specified initialization function. `F` is a template parameter. + * `thread_pool(F&& init)`: Construct a new thread pool with the default number of threads and the specified initialization function. `F` is a template parameter. * `thread_pool(std::size_t num_threads, F&& init)`: Construct a new thread pool with the specified number of threads and the specified initialization function. * Resetters: - * `void reset()`: Reset the pool with a number of threads equal to `std::thread::hardware_concurrency()`, waiting for running tasks first, and preserving submitted tasks after the reset. + * `void reset()`: Reset the pool with the default number of threads (as if constructed with the default constructor). Waits for all tasks first; if pausing is enabled, waits only for running tasks, and queued tasks resume after the pool is reset. If the pool was paused before resetting it, the new pool will be paused as well. * `void reset(std::size_t num_threads)`: Reset the pool with a new number of threads. - * `void reset(F&& init)` Reset the pool with a number of threads equal to `std::thread::hardware_concurrency()` and a new initialization function. `F` is a template parameter. + * `void reset(F&& init)`: Reset the pool with the default number of threads and a new initialization function. `F` is a template parameter. * `void reset(std::size_t num_threads, F&& init)`: Reset the pool with a new number of threads and a new initialization function. * Setters: * `void set_cleanup_func(F&& cleanup)`: Set the thread pool's cleanup function. `F` is a template parameter. @@ -3078,16 +3195,20 @@ Descriptions of each item can also be found in the [Doxygen](https://www.doxygen * `std::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()`. * `std::size_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()` (or `std::jthread::get_id()` in C++20 and later). -* Task submission without futures (`T1`, `T2`, and `F` are template parameters): +* Task submission without futures (`T1`, `T2`, `F`, `C`, and `I` are template parameters): * `void detach_task(F&& task)`: Submit a function with no arguments and no return value into the task queue. To submit a function with arguments, enclose it in a lambda expression. - * `void detach_blocks(T1 first_index, T2 index_after_last, F&& block, std::size_t num_blocks = 0)`: Parallelize a loop by automatically splitting it into blocks. The block function takes two arguments, the start and end of the block, so that it is only called once per block, but it is up to the user make sure the block function correctly deals with all the indices in each block. + * `void detach_blocks(T1 first_index, T2 index_after_last, F&& block, std::size_t num_blocks = 0)`: Parallelize a loop by automatically splitting it into blocks. The block function takes two arguments, the start and end of the block, so that it is only called once per block, but it is up to the user to make sure the block function correctly deals with all the indices in each block. * `void detach_loop(T1 first_index, T2 index_after_last, F&& loop, std::size_t num_blocks = 0)`: Parallelize a loop by automatically splitting it into blocks. The loop function takes one argument, the loop index, so that it is called many times per block. - * `void detach_sequence(1T first_index, T2 index_after_last, F&& sequence)`: Submit a sequence of tasks enumerated by indices to the queue. The sequence function takes one argument, the task index, and will be called once per index. -* Task submission with futures (`T1`, `T2`, `F`, and `R` are template parameters): + * `void detach_sequence(T1 first_index, T2 index_after_last, F&& sequence)`: Submit a sequence of tasks enumerated by indices to the queue. The sequence function takes one argument, the task index, and will be called once per index. + * `void detach_bulk(C& container)`: Submit a container of functions with no arguments and no return values to the queue. + * `void detach_bulk(I first, I last)`: Submit an iterator range containing functions with no arguments and no return values to the queue. +* Task submission with futures (`T1`, `T2`, `F`, `R`, `C`, and `I` 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. - * `BS::multi_future submit_blocks(T1 first_index, T2 index_after_last, F&& block, std::size_t num_blocks = 0)`: Parallelize a loop by automatically splitting it into blocks. The block function takes two arguments, the start and end of the block, so that it is only called 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(T1 first_index, T2 index_after_last, F&& loop, std::size_t num_blocks = 0)`: Parallelize a loop by automatically splitting it into blocks. 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(T1 first_index, T2 index_after_last, F&& sequence)`: Submit a sequence of tasks enumerated by indices to the queue. The sequence function takes one argument, the task index, and will be called once per index. Returns a `BS::multi_future` that contains the futures for all of the tasks. + * `BS::multi_future submit_blocks(T1 first_index, T2 index_after_last, F&& block, std::size_t num_blocks = 0)`: Parallelize a loop by automatically splitting it into blocks. The block function takes two arguments, the start and end of the block, so that it is only called once per block, but it is up to the user to 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 the blocks. + * `BS::multi_future submit_loop(T1 first_index, T2 index_after_last, F&& loop, std::size_t num_blocks = 0)`: Parallelize a loop by automatically splitting it into blocks. 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 the blocks. + * `BS::multi_future submit_sequence(T1 first_index, T2 index_after_last, F&& sequence)`: Submit a sequence of tasks enumerated by indices to the queue. The sequence function takes one argument, the task index, and will be called once per index. Returns a `BS::multi_future` that contains the futures for all the tasks. + * `BS::multi_future submit_bulk(C& container)`: Submit a container of functions with no arguments to the queue. Returns a `BS::multi_future` that contains the futures for all the tasks. + * `BS::multi_future submit_bulk(I first, I last)`: Submit an iterator range containing functions with no arguments to the queue. Returns a `BS::multi_future` that contains the futures for all the tasks. * Task management: * `void purge()`: Purge all the tasks waiting in the queue. Please note that there is no way to restore the purged tasks. * Waiting for tasks (`R`, `P`, `C`, and `D` are template parameters): @@ -3099,14 +3220,14 @@ Descriptions of each item can also be found in the [Doxygen](https://www.doxygen ### Optional features and the template parameter -The thread pool has several optional features that must be explicitly enabled by passing a template parameter. The template parameter is a bitmask, so you can enable several features at once by combining them with the bitwise OR operator `|`. The bitmask flags are members of the `BS::tp` enumeration. +The thread pool has several optional features that must be explicitly enabled by passing a template parameter. The template parameter is a bitmask, so you can enable several features at once by combining them with the bitwise OR operator `|`. The bitmask flags are members of the `BS::tp` enumeration class. * **Task priority:** Enabled by turning on the `BS::tp::priority` flag in the template parameter. When enabled, the static member `priority_enabled` will be set to `true`. * 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 all detach and submit functions. If the priority is not specified, the default value will be 0. * The priority is of type `BS::priority_t`, a signed 8-bit integer, with values between -128 and +127. The tasks will be executed in priority order from highest to lowest. Groups of parallelized tasks will all have the same priority. * The enumeration `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 turning on the `BS::tp::pause` flag in the template parameter. When enabled, the static member `pause_enabled` will be set to `true`. 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 pause()`: Pause the pool. The workers will temporarily stop retrieving new tasks out of the queue, although any tasks already executing 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. * **Wait deadlock checks:** Enabled by turning on the `BS::tp::wait_deadlock_checks` flag in the template parameter. When enabled, the static member `wait_deadlock_checks_enabled` will be set to `true`. @@ -3135,19 +3256,19 @@ The native extensions may be enabled by defining the macro `BS_THREAD_POOL_NATIV The native extensions add the following functions to the `BS` namespace: -* `bool set_os_process_affinity(std::vector& affinity)`: Set the processor affinity of the current process. The argument is an `std::vector` where each element corresponds to a logical processor. Returns `true` if the affinity was set successfully, `false` otherwise. Does not work on macOS. +* `bool BS::set_os_process_affinity(std::vector& affinity)`: Set the processor affinity of the current process. The argument is an `std::vector` where each element corresponds to a logical processor. Returns `true` if the affinity was set successfully, `false` otherwise. Does not work on macOS. * `std::optional> BS::get_os_process_affinity()`: Get the processor affinity of the current process. The optional object will not have a value if the affinity could not be determined. Does not work on macOS. * `bool BS::set_os_process_priority(BS::os_process_priority priority)`: Set the priority of the current process. The argument must be a member of the `BS::os_process_priority` enumeration, which contains the options `idle`, `below_normal`, `normal`, `above_normal`, `high`, and `realtime`. Returns `true` if the priority was set successfully, or `false` otherwise. * `std::optional BS::get_os_process_priority()`: Get the priority of the current process. The optional object will not have a value if the priority could not be determined, or it is not one of the pre-defined values in the `BS::os_process_priority` enumeration. The native extensions also add the following static member functions to `BS::this_thread`: -* `bool set_os_thread_affinity(std::vector& affinity)`: Set the processor affinity of the current thread. The argument is an `std::vector` where each element corresponds to a logical processor. Note that the thread affinity must be a subset of the process affinity for the containing process of a thread. Does not work on macOS. -* `std::optional> get_os_thread_affinity()`: Get the processor affinity of the current thread. The optional object will not have a value if the affinity could not be determined. Does not work on macOS. -* `bool set_os_thread_name(std::string& name)`: Set the name of the current thread. Note that on Linux thread names are limited to 16 characters, including the null terminator. Returns `true` if the name was set successfully, `false` otherwise. -* `std::optional get_os_thread_name()`: Get the name of the current thread. The optional object will not have a value if the name could not be determined. -* `bool set_os_thread_priority(BS::os_thread_priority priority)`: Set the priority of the current thread. The argument must be a member of the `BS::os_thread_priority` enumeration, which contains the options `idle`, `lowest`, `below_normal`, `normal`, `above_normal`, `highest`, and `realtime`. Returns `true` if the priority was set successfully, or `false` otherwise. -* `std::optional get_os_thread_priority()`: Get the priority of the current thread. The optional object will not have a value if the priority could not be determined, or it is not one of the pre-defined values in the `BS::os_thread_priority` enumeration. +* `bool BS::this_thread::set_os_thread_affinity(std::vector& affinity)`: Set the processor affinity of the current thread. The argument is an `std::vector` where each element corresponds to a logical processor. Note that the thread affinity must be a subset of the process affinity for the containing process of a thread. Does not work on macOS and Android. +* `std::optional> BS::this_thread::get_os_thread_affinity()`: Get the processor affinity of the current thread. The optional object will not have a value if the affinity could not be determined. Does not work on macOS and Android. +* `bool BS::this_thread::set_os_thread_name(std::string& name)`: Set the name of the current thread. Note that on Linux thread names are limited to 16 characters, including the null terminator. Returns `true` if the name was set successfully, `false` otherwise. +* `std::optional BS::this_thread::get_os_thread_name()`: Get the name of the current thread. The optional object will not have a value if the name could not be determined. +* `bool BS::this_thread::set_os_thread_priority(BS::os_thread_priority priority)`: Set the priority of the current thread. The argument must be a member of the `BS::os_thread_priority` enumeration, which contains the options `idle`, `lowest`, `below_normal`, `normal`, `above_normal`, `highest`, and `realtime`. Returns `true` if the priority was set successfully, or `false` otherwise. +* `std::optional BS::this_thread::get_os_thread_priority()`: Get the priority of the current thread. The optional object will not have a value if the priority could not be determined, or it is not one of the pre-defined values in the `BS::os_thread_priority` enumeration. Finally, the native extensions add the following member function to `BS::thread_pool`: @@ -3208,9 +3329,7 @@ The library defines the following `constexpr` variables: When the library is imported as a C++20 module using `import BS.thread_pool`, it exports the following names, in alphabetical order: -* `BS::binary_semaphore` * `BS::common_index_type_t` -* `BS::counting_semaphore` * `BS::light_thread_pool` * `BS::multi_future` * `BS::pause_thread_pool` @@ -3224,11 +3343,14 @@ When the library is imported as a C++20 module using `import BS.thread * `BS::thread_pool_module` * `BS::thread_pool_native_extensions` * `BS::thread_pool_version` -* `BS::tp` +* `BS::tp` (plus related bitwise operators) * `BS::version` -* `BS::wait_deadlock` * `BS::wdc_thread_pool` +If exceptions are enabled, the following names are also exported: + +* `BS::wait_deadlock` + If the native extensions are enabled, the following names are also exported: * `BS::get_os_process_affinity` @@ -3242,7 +3364,7 @@ If the native extensions are enabled, the following names are also exported: ### The `compile_cpp.py` script -The Python script `compile_cpp.py`, in the `scripts` folder of [the GitHub repository](https://github.com/bshoshany/thread-pool), can be used to compile any C++ source file with different compilers on different platforms. It requires Python 3.12 or later. +The Python script `compile_cpp.py`, in the `scripts` folder of [the GitHub repository](https://github.com/bshoshany/thread-pool), can be used to compile any C++ source file with different compilers on different platforms. It has only been tested using Python v3.14.2, the latest version at the time of writing, and may not work with older versions. The script was written by the author of the library to make it easier to test the library with different combinations of compilers, standards, and platforms using the built-in Visual Studio Code tasks. However, note that this script is not meant to replace CMake or any full-fledged build system, it's just a convenient script for developing single-header libraries like this one or other small projects. @@ -3253,51 +3375,56 @@ The compilation parameters can be configured using the command line arguments an * Positional argument(s): the source file(s) to compile. * `-h` or `--help`: Show the help message and exit. * `-a` or `--arch`: The target architecture (MSVC only). Must be one of `[amd64, arm64]`, default is `amd64`. +* `-b` or `--clear-output`: Clear the output folder before compiling. If no source files are specified, just clear and exit. The outcome is always an empty output folder. * `-c` or `--compiler`: Which compiler to use. Must be one of `[cl, clang++, g++]`. The default is to determine it automatically based on the platform. * `-d` or `--define`: Macros to define. Use this argument multiple times to define more than one macro. Additional macros can be defined in `compile_cpp.yaml`. +* `-e` or `--force`: Force recompilation even if the executable is up to date. * `-f` or `--flag`: Extra compiler flags to add. Use this argument multiple times to add more than one flag. Additional flags can be specified in `compile_cpp.yaml`. +* `-g` or `--ignore-config`: Ignore the `compile_cpp.yaml` configuration file, if it exists. * `-i` or `--include`: The include folder to use. Use this argument multiple times to use more than one include folder. Additional include folders can be specified in `compile_cpp.yaml`. * `-l` or `--as-module`: Enable this flag to compile the file as a C++20 module. -* `-m` or `--module`: C++20 module files to use if desired, in the format `module_name=module_file,dependent_files,...`. Use this argument multiple times to use more than one module. Additional modules can be specified in `compile_cpp.yaml`. The dependent files are only used to determine whether the module needs to be recompiled. -* `-o` or `--output`: The output folder and/or executable name. End with `/` to create the folder if it doesn't already exist. If not specified, the folder defined in `compile_cpp.yaml` will be used. If the executable name is not specified, it will be determined automatically in the format `source_[module_]type-compiler-standard` where: +* `-m` or `--module`: C++20 module files to use if desired, in the format `module_name=module_file,dependencies,...`. Use this argument multiple times to use more than one module. Additional modules can be specified in `compile_cpp.yaml`. The dependencies are only used to determine whether the module needs to be recompiled. +* `-n` or `--deps`: Dependencies used to detect if recompilation is needed. If these files are modified, then the executable is recompiled even if the source files have not been modified. Use this argument multiple times to add more than one dependency. Additional dependencies can be specified in `compile_cpp.yaml`. Note that this is not used for C++20 modules, which have their own dependencies, listed when using `-m`. +* `-o` or `--output`: The output folder and/or executable name. End with `/` to create the folder if it doesn't already exist. If not specified, the folder defined in `compile_cpp.yaml` will be used. If the executable name is not specified, it will be determined automatically in the format `{source}_[module_]{type}-{compiler}-{standard}` where: * `source` is the name of the first source file (without the extension). - * `module_`, if present, indicates that the file is a C++20 module. + * `module_`, if present, indicates that the file is a C++20 module (if `-l`/`--as-module` is enabled) * `type` is one of `[debug, release]`. * `compiler` is one of `[clang, gcc, msvc]`. - * `standard` is one of `[c++17, c++20, c++23]`. -* `-p` or `--pass`: Pass command line arguments to the compiled program when running it, if `-r` is specified. Use this argument multiple times to pass more than one argument to the program. Additional arguments can be specified in `compile_cpp.yaml`. + * `standard` is one of `[cpp17, cpp20, cpp23]`. +* `-p` or `--pass`: Pass command line arguments to the compiled program when running it, if `-r`/`--run` is specified. Use this argument multiple times to pass more than one argument to the program. Additional arguments can be specified in `compile_cpp.yaml`. * `-r` or `--run`: Enable this flag to run the program after compiling it. * `-s` or `--std`: Which C++ standard to use. Must be one of `[c++17, c++20, c++23]`. The default is `c++23`. * `-t` or `--type`: Which mode to compile in. Must be one of `[debug, release]`. The default is `debug`. * `-u` or `--std-module`: Specify the path to the standard library module (C++23 only). Taken from `compile_cpp.yaml` if not specified. Use `auto` to auto-detect or `disable` to explicitly disable. * `-v` or `--verbose`: Enable this flag to print the script's diagnostic messages. +* `-x` or `--disable-exceptions`: If set to `true`, disables exceptions in the compiler flags. If set to `false`, exceptions will be enabled. If not specified, the setting will be taken from `compile_cpp.yaml`. +* `-y` or `--try-all`: Test compilation using all possible combinations of compilers and C++ standards available in the system. Also runs each compiled program if `-r`/`--run` is specified. All other arguments are passed to all compilation attempts. Cannot be used together with `-c`/`--compiler` or `-s`/`--std`. The `compile_cpp.yaml` file includes the following fields: * `defines`: A list of macros to define when compiling the source files. +* `deps`: A list of dependencies, such as header files or libraries. All source files compiled using this script will be recompiled if any of these files change. +* `disable_exceptions`: Whether to disable exceptions in the compiler flags. Defaults to `false` if not specified. * `flags`: A map of flags to pass to each compiler. The compiler should be one of `[cl, clang++, g++]`. The flags should be a list of strings. * `includes`: A list of include folders. -* `modules`: A map of C++20 modules in the format `module_name: [module_path, dependent files, ...]`. Will only be used in C++20 or C++23 mode. The dependent files are only used to determine whether the module needs to be recompiled. +* `modules`: A map of C++20 modules in the format `module_name: [module_path, dependencies, ...]`. Will only be used in C++20 or C++23 mode. The dependencies are only used to determine whether the module needs to be recompiled. * `output`: The output folder for the compiled files. * `pass_args`: A list of arguments to pass to the program if running it after compilation. -* `std_module`: A map of paths to the standard library modules for each OS and compiler combination (C++23 only). The OS should be one of `[Windows, Linux, Darwin]`. Use `Automatic` to determine the path automatically if possible. +* `std_module`: A map of paths to the standard library modules for each OS and compiler combination (C++23 only). The OS should be one of `[Windows, Linux, Darwin]`. Use `auto` to determine the path automatically if possible. Please see the `compile_cpp.yaml` file in the GitHub repository for an example of how to use it. -### Other included tools +By default, the script prints colored output using ANSI escape codes for better readability. This can be disabled by setting the `NO_COLOR` environment variable. -The `scripts` folder of [the GitHub repository](https://github.com/bshoshany/thread-pool) contains two other Python scripts that are used in the development of the library: +### Visual Studio Code tasks -* `test_all.py` performs the [automated tests](#automated-tests) in C++17, C++20, and C++23 modes, using all compilers available in the system (Clang, GCC, and/or MSVC). Since there are so many tests, the test script does not perform the benchmarks, as that would take too long. Pass the optional argument `--compile-only` to only check that the program compiles successfully with all compilers, without running it. -* `clear_folder.py` is used to clean up output and temporary folders. It will create the folder if it does not already exist, so the outcome is always an empty folder. - -In addition, for Visual Studio Code users, the GitHub repository includes three `.vscode` folders: +For Visual Studio Code users, the GitHub repository includes three `.vscode` folders: * `.vscode-windows`, to be used in Windows with Clang, GCC, and MSVC. * `.vscode-linux`, to be used in Linux with Clang and GCC. * `.vscode-macos`, to be used in macOS with LLVM Clang (not Apple Clang). -Each folder contains appropriate `c_cpp_properties.json`, `launch.json`, and `tasks.json` files that utilize the included Python scripts. Users are welcome to use these files in their own projects, but they may require some modifications to work on specific systems. +Each folder contains appropriate `c_cpp_properties.json`, `launch.json`, and `tasks.json` files that utilize the included Python script [`compile_cpp.py`](#the-compile_cpppy-script). Users are welcome to use these files in their own projects, but they may require some modifications to work on specific systems. ## About the project @@ -3319,7 +3446,7 @@ Many GitHub users have helped improve this project, directly or indirectly, via ### Copyright and citing -Copyright (c) 2024 [Barak Shoshany](https://baraksh.com/). Licensed under the [MIT license](https://github.com/bshoshany/thread-pool/blob/master/LICENSE.txt). +Copyright (c) 2021-2026 [Barak Shoshany](https://baraksh.com/). Licensed under the [MIT license](https://github.com/bshoshany/thread-pool/blob/master/LICENSE.txt). 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. diff --git a/compile_cpp.yaml b/compile_cpp.yaml index e7970c7..19d7b57 100644 --- a/compile_cpp.yaml +++ b/compile_cpp.yaml @@ -1,25 +1,31 @@ # A list of macros to define when compiling the source files. defines: [BS_THREAD_POOL_TEST_IMPORT_MODULE, BS_THREAD_POOL_IMPORT_STD, BS_THREAD_POOL_NATIVE_EXTENSIONS] +# A list of dependencies, such as header files or libraries. All source files compiled using this script will be recompiled if any of these files change. (Note that this is not used for C++20 modules, which have their own dependencies, listed in the modules map.) +deps: [include/BS_thread_pool.hpp] +# Whether to disable exceptions. +disable_exceptions: false # A map of flags to pass to each compiler. The compiler should be one of [cl, clang++, g++]. The flags should be a list of strings. flags: - cl: [/W4] - clang++: [-Wall, -Wextra, -Wconversion, -Wsign-conversion, -Wpedantic, -Wshadow, -Weffc++, -march=native, -fcolor-diagnostics, -fansi-escape-codes, -stdlib=libc++] - g++: [-Wall, -Wextra, -Wconversion, -Wpedantic, -Wshadow, -Wuseless-cast, -march=native, -fdiagnostics-color=always] + cl: [/W4, /D_CRT_SECURE_NO_WARNINGS] + clang++: [-Wall, -Wextra, -Wconversion, -Wsign-conversion, -Wpedantic, -Wshadow, -Weffc++, -Wdocumentation, -march=native, -fcolor-diagnostics, -fansi-escape-codes, -stdlib=libc++] + g++: [-Wall, -Wextra, -Wconversion, -Wsign-conversion, -Wpedantic, -Wshadow, -Wuseless-cast, -march=native, -fdiagnostics-color=always, -Wnrvo] # A list of include folders. includes: [include] -# A map of C++20 modules in the format "module_name: [module_path, dependent files, ...]". Will only be used in C++20 or C++23 mode. The dependent files are any files that the module depends on, and are only used to determine whether the module needs to be recompiled. +# A map of C++20 modules in the format "module_name: [module_path, dependencies, ...]". Will only be used in C++20 or C++23 mode. The dependencies are any files that the module depends on, and are only used to determine whether the module needs to be recompiled. modules: BS.thread_pool: [modules/BS.thread_pool.cppm, include/BS_thread_pool.hpp] # The output folder for the compiled files. output: build/ # A list of arguments to pass to the program if running it after compilation. pass_args: [] -# A map of paths to the standard library modules for each OS and compiler combination (C++23 only). The OS should be one of [Darwin, Linux, Windows]. This is currently only officially supported by MSVC with Microsoft STL and LLVM Clang (NOT Apple Clang) with LLVM libc++. It is not supported by GCC with any standard library, or any compiler with GNU libstdc++. Use "auto" to determine the path automatically if possible. +# A map of paths to the standard library modules for each OS and compiler combination (C++23 only). The OS should be one of [Darwin, Linux, Windows]. Use "auto" to determine the path automatically if possible. std_module: Darwin: clang++: auto Linux: clang++: auto + g++: auto Windows: cl: auto clang++: auto + g++: auto diff --git a/include/BS_thread_pool.hpp b/include/BS_thread_pool.hpp index e9bb7ce..e6a129f 100644 --- a/include/BS_thread_pool.hpp +++ b/include/BS_thread_pool.hpp @@ -7,9 +7,9 @@ * * @file BS_thread_pool.hpp * @author Barak Shoshany (baraksh@gmail.com) (https://baraksh.com/) - * @version 5.0.0 - * @date 2024-12-19 - * @copyright Copyright (c) 2024 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.1016/j.softx.2024.101687, SoftwareX 26 (2024) 101687, arXiv:2105.00613 + * @version 5.1.0 + * @date 2026-01-03 + * @copyright Copyright (c) 2021-2026 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.1016/j.softx.2024.101687, SoftwareX 26 (2024) 101687, arXiv:2105.00613 * * @brief `BS::thread_pool`: a fast, lightweight, modern, and easy-to-use C++17/C++20/C++23 thread pool library. This header file contains the entire library, and is the only file needed to use the library. */ @@ -17,20 +17,58 @@ #ifndef BS_THREAD_POOL_HPP #define BS_THREAD_POOL_HPP -// We need to include since if we're using `import std` it will not define any feature-test macros, including `__cpp_lib_modules`, which we need to check if `import std` is supported in the first place. +// We need to include since if we're using `import std` it will not define any feature-test macros. #ifdef __has_include #if __has_include() #include // NOLINT(misc-include-cleaner) #endif #endif -// If the macro `BS_THREAD_POOL_IMPORT_STD` is defined, import the C++ Standard Library as a module. Otherwise, include the relevant Standard Library header files. This is currently only officially supported by MSVC with Microsoft STL and LLVM Clang (NOT Apple Clang) with LLVM libc++. It is not supported by GCC with any standard library, or any compiler with GNU libstdc++. We also check that the feature is enabled by checking `__cpp_lib_modules`. However, MSVC defines this macro even in C++20 mode, which is not standards-compliant, so we check that we are in C++23 mode; MSVC currently reports `__cplusplus` as `202004L` for C++23 mode, so we use that value. -#if defined(BS_THREAD_POOL_IMPORT_STD) && defined(__cpp_lib_modules) && (__cplusplus >= 202004L) && (defined(_MSC_VER) || (defined(__clang__) && defined(_LIBCPP_VERSION) && !defined(__apple_build_version__))) +// At the time of this release, there is a bug in Clang with libc++ where using `std::jthread` in a C++20 module causes a compilation error. As a workaround, until the bug is fixed, the thread pool library automatically falls back to `std::thread` if it detects that Clang and libc++ are being used together with C++20 modules. This workaround can be disabled by defining `BS_THREAD_POOL_DISABLE_WORKAROUNDS` when compiling the module. TODO: Remove this workaround when the bug is fixed. +#if defined(__clang__) && defined(_LIBCPP_VERSION) && defined(BS_THREAD_POOL_MODULE) && (__cplusplus >= 202002L) && !defined(BS_THREAD_POOL_DISABLE_WORKAROUNDS) + #ifdef __cpp_lib_jthread + #undef __cpp_lib_jthread + #endif +#endif + +// At the time of this release, there is a bug when using GCC with libstdc++ on Windows via MSYS2 where the `BS.thread_pool` module doesn't compile if both native extensions and `import std` are enabled. As a workaround, until the bug is fixed, the thread pool library automatically falls back to header files if it detects that GCC and libstdc++ are being used together with the C++23 `std` module on Windows. This workaround can be disabled by defining `BS_THREAD_POOL_DISABLE_WORKAROUNDS` when compiling the module. TODO: Remove this workaround when the bug is fixed. +#if (defined(__GNUC__) && defined(_GLIBCXX_RELEASE) && defined(_WIN32)) && !defined(BS_THREAD_POOL_DISABLE_WORKAROUNDS) + #ifdef BS_THREAD_POOL_IMPORT_STD + #undef BS_THREAD_POOL_IMPORT_STD + #endif +#endif + +// In GCC with libstdc++ on Linux, loading the system headers after `import std` causes compilation errors, so we load them first. +#ifdef BS_THREAD_POOL_NATIVE_EXTENSIONS + #if defined(_WIN32) + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + #ifndef NOMINMAX + #define NOMINMAX + #endif + #include + #elif defined(__linux__) || defined(__APPLE__) + #include + #include + #include + #include + #if defined(__linux__) + #include + #include + #endif + #else + #undef BS_THREAD_POOL_NATIVE_EXTENSIONS + #endif +#endif + +// If the macro `BS_THREAD_POOL_IMPORT_STD` is defined, import the C++ Standard Library as a module. Otherwise, include the relevant Standard Library header files. +#if defined(BS_THREAD_POOL_IMPORT_STD) && (__cplusplus >= 202004L) // Only allow importing the `std` module if the library itself is imported as a module. If the library is included as a header file, this will force the program that included the header file to also import `std`, which is not desirable and can lead to compilation errors if the program `#include`s any Standard Library header files. #ifdef BS_THREAD_POOL_MODULE import std; #else - #error "The thread pool library cannot import the C++ Standard Library as a module using `import std` if the library itself is not imported as a module. Either use `import BS.thread_pool` to import the libary, or remove the `BS_THREAD_POOL_IMPORT_STD` macro. Aborting compilation." + #error "The thread pool library cannot import the C++ Standard Library as a module using `import std` if the library itself is not imported as a module. Either use `import BS.thread_pool` to import the library, or remove the `BS_THREAD_POOL_IMPORT_STD` macro. Aborting compilation." #endif #else #undef BS_THREAD_POOL_IMPORT_STD @@ -69,41 +107,25 @@ import std; #ifdef __cpp_lib_int_pow2 #include #endif - #ifdef __cpp_lib_semaphore - #include - #endif #ifdef __cpp_lib_jthread #include #endif #endif -#ifdef BS_THREAD_POOL_NATIVE_EXTENSIONS - #if defined(_WIN32) - #include - #undef min - #undef max - #elif defined(__linux__) || defined(__APPLE__) - #include - #include - #include - #include - #if defined(__linux__) - #include - #include - #endif - #else - #undef BS_THREAD_POOL_NATIVE_EXTENSIONS - #endif +// On Linux, defines macros called `major` and `minor`, which we undefine here to prevent conflicts. +#ifdef major + #undef major +#endif +#ifdef minor + #undef minor #endif -#if defined(__linux__) - // On Linux, defines macros called `major` and `minor`. We undefine them here so the `version` struct can work. - #ifdef major - #undef major - #endif - #ifdef minor - #undef minor - #endif +// On Windows, defines macros called `min` and `max`, which we undefine here to prevent conflicts. +#ifdef min + #undef min +#endif +#ifdef max + #undef max #endif /** @@ -112,13 +134,13 @@ import std; namespace BS { // Macros indicating the version of the thread pool library. #define BS_THREAD_POOL_VERSION_MAJOR 5 -#define BS_THREAD_POOL_VERSION_MINOR 0 +#define BS_THREAD_POOL_VERSION_MINOR 1 #define BS_THREAD_POOL_VERSION_PATCH 0 /** * @brief A struct used to store a version number, which can be checked and compared at compilation time. */ -struct version +struct [[nodiscard]] version { constexpr version(const std::uint64_t major_, const std::uint64_t minor_, const std::uint64_t patch_) noexcept : major(major_), minor(minor_), patch(patch_) {} @@ -221,27 +243,126 @@ inline constexpr bool thread_pool_native_extensions = false; */ using opt_t = std::uint8_t; -template +/** + * @brief An enumeration class of flags to be used in the bitmask template parameter of `BS::thread_pool` to enable optional features. + */ +enum class tp : opt_t +{ + /** + * @brief No optional features enabled. + */ + none = 0, + + /** + * @brief Enable task priority. + */ + priority = 1 << 0, + + /** + * @brief Enable pausing. + */ + pause = 1 << 1, + + /** + * @brief Enable wait deadlock checks. + */ + wait_deadlock_checks = 1 << 2 +}; + +// NOLINTBEGIN(bugprone-macro-parentheses) +#define BS_THREAD_POOL_DEFINE_BITWISE_OPERATOR(ENUM, OP) \ + constexpr ENUM operator OP(const ENUM lhs, const ENUM rhs) noexcept \ + { \ + return static_cast(static_cast>(lhs) OP static_cast>(rhs)); \ + } \ + constexpr ENUM& operator OP##=(ENUM& lhs, const ENUM rhs) noexcept \ + { \ + return lhs = lhs OP rhs; \ + } +// NOLINTEND(bugprone-macro-parentheses) + +BS_THREAD_POOL_DEFINE_BITWISE_OPERATOR(tp, &) +BS_THREAD_POOL_DEFINE_BITWISE_OPERATOR(tp, |) +BS_THREAD_POOL_DEFINE_BITWISE_OPERATOR(tp, ^) + +constexpr tp operator~(const tp value) noexcept +{ + return static_cast(~static_cast>(value)); +} + +template class thread_pool; #ifdef __cpp_lib_move_only_function /** * @brief The template to use to store functions in the task queue and other places. In C++23 and later we use `std::move_only_function`. */ -template -using function_t = std::move_only_function; +using std::move_only_function; #else +template +class move_only_function; + /** - * @brief The template to use to store functions in the task queue and other places. In C++17 we use `std::function`. + * @brief A simple polyfill for `std::move_only_function`, to be used if C++23 features are not available. Note that it does not have all the features of `std::move_only_function`, only the minimum needed for the thread pool library. + * + * @tparam R The return type of the function. + * @tparam Args The argument types of the function. */ -template -using function_t = std::function; +template +class move_only_function +{ +public: + move_only_function() = default; + move_only_function(move_only_function&&) noexcept = default; + move_only_function& operator=(move_only_function&&) noexcept = default; + move_only_function(const move_only_function&) = delete; + move_only_function& operator=(const move_only_function&) = delete; + ~move_only_function() = default; + + template , move_only_function> && std::is_invocable_r_v>> + move_only_function(F&& func) : ptr(std::make_unique>>(std::forward(func))) {} // NOLINT(hicpp-explicit-conversions) + + R operator()(Args... args) + { + return ptr->call(std::forward(args)...); + } + +private: + struct func_concept + { + virtual ~func_concept() = default; + virtual R call(Args... args) = 0; + }; + + template + struct func_model final : func_concept + { + template , func_model>>> + explicit func_model(T&& func) : stored_func(std::forward(func)) {} + + R call(Args... args) override + { + if constexpr (std::is_void_v) + { + std::invoke(stored_func, std::forward(args)...); + } + else + { + return std::invoke(stored_func, std::forward(args)...); + } + } + + F stored_func; + }; + + std::unique_ptr ptr = nullptr; +}; #endif /** * @brief The type of tasks in the task queue. */ -using task_t = function_t; +using task_t = move_only_function; #ifdef __cpp_lib_jthread /** @@ -255,7 +376,7 @@ using thread_t = std::jthread; #define BS_THREAD_POOL_OR_STOP_CONDITION #else /** - * @brief The type of threads to use. In C++17 we use`std::thread`. + * @brief The type of threads to use. In C++17 we use `std::thread`. */ using thread_t = std::thread; // The following macros are used to determine how to stop the workers. In C++17 we use a manual flag `workers_running`. @@ -273,7 +394,7 @@ using priority_t = std::int8_t; /** * @brief An enum containing some pre-defined priorities for convenience. */ -enum pr : priority_t +enum pr : priority_t // NOLINT(cppcoreguidelines-use-enum-class) This cannot be an `enum class` because we need the numerical values. { lowest = -128, low = -64, @@ -308,9 +429,9 @@ struct [[nodiscard]] pr_task } /** - * @brief The task. + * @brief The task. It is `mutable` so it can be moved out of the `const` reference returned by `std::priority_queue::top()`. */ - task_t task; + mutable task_t task; /** * @brief The priority of the task. @@ -437,7 +558,7 @@ public: for (const std::future& future : *this) { future.wait_until(timeout_time); - if (timeout_time < std::chrono::steady_clock::now()) + if (timeout_time < C::now()) return false; } return true; @@ -460,7 +581,7 @@ public: * @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. */ - blocks(const T first_index_, const T index_after_last_, const std::size_t num_blocks_) noexcept : first_index(first_index_), index_after_last(index_after_last_), num_blocks(num_blocks_) + blocks(const T first_index_, const T index_after_last_, const std::size_t num_blocks_) noexcept : num_blocks(num_blocks_), first_index(first_index_), index_after_last(index_after_last_) { if (index_after_last > first_index) { @@ -518,16 +639,6 @@ private: */ std::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. */ @@ -537,13 +648,130 @@ private: * @brief The remainder obtained after dividing the total size by the number of blocks. */ std::size_t remainder = 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; }; // class blocks +/** + * @brief A function object class used by `detach_blocks()` and `submit_blocks()` to execute a block function over a specified range of indices. + * + * @tparam T The type of the indices. + * @tparam F The type of the function. + * @tparam R The return type of the function (can be `void`). + */ +template +struct block_task +{ + R operator()() + { + return (*block_ptr)(start, end); + } + + std::shared_ptr> block_ptr; + T start; + T end; +}; // struct block_task + +/** + * @brief A function object class used by `detach_loop()` and `submit_loop()` to execute a loop function over a specified range of indices. + * + * @tparam T The type of the indices. + * @tparam F The type of the function. + */ +template +struct loop_task +{ + void operator()() + { + for (T i = start; i < end; ++i) + (*loop_ptr)(i); + } + + std::shared_ptr> loop_ptr; + T start; + T end; +}; // struct loop_task + +/** + * @brief A function object class used by `detach_sequence()` and `submit_sequence()` to execute a sequence function over a specified index. + * + * @tparam T The type of the index. + * @tparam F The type of the function. + * @tparam R The return type of the function (can be `void`). + */ +template +struct sequence_task +{ + R operator()() + { + return (*sequence_ptr)(i); + } + + std::shared_ptr> sequence_ptr; + T i; +}; // struct sequence_task + +/** + * @brief A class that takes a function with a return value (but no arguments), and constructs a task with no return value along with a future used to retrieve the function's return value once the task is executed. Used by `submit_task()` and `submit_bulk()`. + * + * @tparam R The return type of the function (can be `void`). + */ +template +struct task_and_future +{ + template , task_and_future>>> + explicit task_and_future(F&& func) + { + std::promise promise; + future = promise.get_future(); + task = [task = std::forward(func), promise = std::move(promise)]() mutable + { +#ifdef __cpp_exceptions + try + { +#endif + if constexpr (std::is_void_v) + { + task(); + promise.set_value(); + } + else + { + promise.set_value(task()); + } +#ifdef __cpp_exceptions + } + catch (...) + { + try + { + promise.set_exception(std::current_exception()); + } + catch (...) + { + } + } +#endif + }; + } + + std::future future; + task_t task; +}; // struct task_and_future + #ifdef __cpp_exceptions /** - * @brief An exception that will be thrown by `wait()`, `wait_for()`, and `wait_until()` if the user tries to call them from within a thread of the same pool, which would result in a deadlock. Only used if the flag `BS:tp::wait_deadlock_checks` is enabled in the template parameter of `BS::thread_pool`. + * @brief An exception that will be thrown by `wait()`, `wait_for()`, and `wait_until()` if the user tries to call them from within a thread of the same pool, which would result in a deadlock. Only used if the flag `BS::tp::wait_deadlock_checks` is enabled in the template parameter of `BS::thread_pool`. */ -struct wait_deadlock : public std::runtime_error +struct [[nodiscard]] wait_deadlock : public std::runtime_error { wait_deadlock() : std::runtime_error("BS::wait_deadlock") {}; }; @@ -656,7 +884,7 @@ enum class os_thread_priority * @param affinity The processor affinity to set, as an `std::vector` where each element corresponds to a logical processor. * @return `true` if the affinity was set successfully, `false` otherwise. On macOS, this function always returns `false`. */ -inline bool set_os_process_affinity(const std::vector& affinity) +inline bool set_os_process_affinity([[maybe_unused]] const std::vector& affinity) { #if defined(_WIN32) DWORD_PTR process_mask = 0; @@ -673,7 +901,7 @@ inline bool set_os_process_affinity(const std::vector& affinity) } return sched_setaffinity(getpid(), sizeof(cpu_set_t), &cpu_set) == 0; #elif defined(__APPLE__) - return affinity[0] && false; // NOLINT(readability-simplify-boolean-expr) // Using `affinity` to suppress unused parameter warning. + return false; #endif } @@ -733,11 +961,11 @@ inline bool set_os_process_priority(const os_process_priority priority) #endif /** - * @brief A class used to obtain information about the current thread and, if native extensions are enabled, set its priority and affinity. + * @brief A class used to obtain information about the current thread and, if native extensions are enabled, get/set its priority, affinity, or name. */ class [[nodiscard]] this_thread { - template + template friend class thread_pool; public: @@ -763,9 +991,9 @@ public: #ifdef BS_THREAD_POOL_NATIVE_EXTENSIONS /** - * @brief Get the processor affinity of the current thread using the current platform's native API. This should work on Windows and Linux, but is not possible on macOS as the native API does not allow it. + * @brief Get the processor affinity of the current thread using the current platform's native API. This should work on Windows and Linux, but is not possible on macOS and Android as the native API does not allow it. * - * @return An `std::optional` object, optionally containing the processor affinity of the current thread as an `std::vector` where each element corresponds to a logical processor. If the returned object does not contain a value, then the affinity could not be determined. On macOS, this function always returns `std::nullopt`. + * @return An `std::optional` object, optionally containing the processor affinity of the current thread as an `std::vector` where each element corresponds to a logical processor. If the returned object does not contain a value, then the affinity could not be determined. On macOS and Android, this function always returns `std::nullopt`. */ [[nodiscard]] static std::optional> get_os_thread_affinity() { @@ -794,7 +1022,7 @@ public: for (std::size_t i = 0; i < num_cpus; ++i) affinity[i] = ((previous_mask & (1ULL << i)) != 0ULL); return affinity; - #elif defined(__linux__) + #elif defined(__linux__) && !defined(__ANDROID__) cpu_set_t cpu_set; CPU_ZERO(&cpu_set); if (pthread_getaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpu_set) != 0) @@ -806,25 +1034,25 @@ public: for (std::size_t i = 0; i < affinity.size(); ++i) affinity[i] = CPU_ISSET(i, &cpu_set); return affinity; - #elif defined(__APPLE__) + #else return std::nullopt; #endif } /** - * @brief Set the processor affinity of the current thread using the current platform's native API. This should work on Windows and Linux, but is not possible on macOS as the native API does not allow it. Note that the thread affinity must be a subset of the process affinity (as obtained using `BS::get_os_process_affinity()`) for the containing process of a thread. + * @brief Set the processor affinity of the current thread using the current platform's native API. This should work on Windows and Linux, but is not possible on macOS and Android as the native API does not allow it. Note that the thread affinity must be a subset of the process affinity (as obtained using `BS::get_os_process_affinity()`) for the containing process of a thread. * * @param affinity The processor affinity to set, as an `std::vector` where each element corresponds to a logical processor. - * @return `true` if the affinity was set successfully, `false` otherwise. On macOS, this function always returns `false`. + * @return `true` if the affinity was set successfully, `false` otherwise. On macOS and Android, this function always returns `false`. */ - static bool set_os_thread_affinity(const std::vector& affinity) + static bool set_os_thread_affinity([[maybe_unused]] const std::vector& affinity) { #if defined(_WIN32) DWORD_PTR thread_mask = 0; for (std::size_t i = 0; i < std::min(affinity.size(), sizeof(DWORD_PTR) * 8); ++i) thread_mask |= (affinity[i] ? (1ULL << i) : 0ULL); return SetThreadAffinityMask(GetCurrentThread(), thread_mask) != 0; - #elif defined(__linux__) + #elif defined(__linux__) && !defined(__ANDROID__) cpu_set_t cpu_set; CPU_ZERO(&cpu_set); for (std::size_t i = 0; i < std::min(affinity.size(), CPU_SETSIZE); ++i) @@ -833,8 +1061,8 @@ public: CPU_SET(i, &cpu_set); } return pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpu_set) == 0; - #elif defined(__APPLE__) - return affinity[0] && false; // NOLINT(readability-simplify-boolean-expr) // Using `affinity` to suppress unused parameter warning. + #else + return false; #endif } @@ -931,7 +1159,7 @@ public: // The only pre-defined priority that uses SCHED_FIFO and the maximum available priority value is the "realtime" priority. return os_thread_priority::realtime; } - if (policy == SCHED_RR && param.sched_priority == sched_get_priority_min(SCHED_RR) + (sched_get_priority_max(SCHED_RR) - sched_get_priority_min(SCHED_RR)) / 2) + if (policy == SCHED_RR && param.sched_priority == sched_get_priority_min(SCHED_RR) + ((sched_get_priority_max(SCHED_RR) - sched_get_priority_min(SCHED_RR)) / 2)) { // The only pre-defined priority that uses SCHED_RR and a priority in the middle of the available range is the "highest" priority. return os_thread_priority::highest; @@ -1028,7 +1256,7 @@ public: case os_thread_priority::highest: // "Highest" pre-defined priority: We use the policy `SCHED_RR` ("round-robin") with a priority in the middle of the available range. policy = SCHED_RR; - param.sched_priority = sched_get_priority_min(SCHED_RR) + (sched_get_priority_max(SCHED_RR) - sched_get_priority_min(SCHED_RR)) / 2; + param.sched_priority = sched_get_priority_min(SCHED_RR) + ((sched_get_priority_max(SCHED_RR) - sched_get_priority_min(SCHED_RR)) / 2); break; case os_thread_priority::above_normal: // "Above normal" pre-defined priority: We use the policy `SCHED_OTHER` (the default). This policy does not accept a priority value, so priority must be 0. However, we set the "nice" value to the minimum value as given by `PRIO_MIN`, plus 2 (which should evaluate to -18). The usual range is -20 to 19 or 20, with higher values corresponding to lower priorities. @@ -1171,32 +1399,6 @@ struct common_index_type && std:: template using common_index_type_t = typename common_index_type::type; -/** - * @brief An enumeration of flags to be used in the bitmask template parameter of `BS::thread_pool` to enable optional features. - */ -enum tp : opt_t -{ - /** - * @brief No optional features enabled. - */ - none = 0, - - /** - * @brief Enable task priority. - */ - priority = 1 << 0, - - /** - * @brief Enable pausing. - */ - pause = 1 << 2, - - /** - * @brief Enable wait deadlock checks. - */ - wait_deadlock_checks = 1 << 3 -}; - /** * @brief A fast, lightweight, modern, and easy-to-use C++17/C++20/C++23 thread pool class. This alias defines a thread pool with all optional features disabled. */ @@ -1222,24 +1424,24 @@ using wdc_thread_pool = thread_pool; * * @tparam OptFlags A bitmask of flags which can be used to enable optional features. The flags are members of the `BS::tp` enumeration: `BS::tp::priority`, `BS::tp::pause`, and `BS::tp::wait_deadlock_checks`. The default is `BS::tp::none`, which disables all optional features. To enable multiple features, use the bitwise OR operator `|`, e.g. `BS::tp::priority | BS::tp::pause`. */ -template +template class [[nodiscard]] thread_pool { public: /** * @brief A flag indicating whether task priority is enabled. */ - static constexpr bool priority_enabled = (OptFlags & tp::priority) != 0; + static constexpr bool priority_enabled = (OptFlags & tp::priority) != tp::none; /** * @brief A flag indicating whether pausing is enabled. */ - static constexpr bool pause_enabled = (OptFlags & tp::pause) != 0; + static constexpr bool pause_enabled = (OptFlags & tp::pause) != tp::none; /** * @brief A flag indicating whether wait deadlock checks are enabled. */ - static constexpr bool wait_deadlock_checks_enabled = (OptFlags & tp::wait_deadlock_checks) != 0; + static constexpr bool wait_deadlock_checks_enabled = (OptFlags & tp::wait_deadlock_checks) != tp::none; #ifndef __cpp_exceptions static_assert(!wait_deadlock_checks_enabled, "Wait deadlock checks cannot be enabled if exception handling is disabled."); @@ -1250,7 +1452,7 @@ public: // ============================ /** - * @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. + * @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. If the native extensions are enabled, the pool will instead use the number of threads available to the process, as obtained from `BS::get_os_process_affinity()`, which can be less than the number of hardware threads. */ thread_pool() : thread_pool(0, [] {}) {} @@ -1262,14 +1464,12 @@ public: explicit thread_pool(const std::size_t num_threads) : thread_pool(num_threads, [] {}) {} /** - * @brief Construct a new thread pool with the specified initialization function. + * @brief Construct a new thread pool with the specified initialization function and the default number of threads. * * @param init An initialization function to run in each thread before it starts executing any submitted tasks. The function must have no return value, and can either take one argument, the thread index of type `std::size_t`, or zero arguments. It will be executed exactly once per thread, when the thread is first constructed. The initialization function must not throw any exceptions, as that will result in program termination. Any exceptions must be handled explicitly within the function. */ template - explicit thread_pool(F&& init) : thread_pool(0, std::forward(init)) - { - } + explicit thread_pool(F&& init) : thread_pool(0, std::forward(init)) {} /** * @brief Construct a new thread pool with the specified number of threads and initialization function. @@ -1315,66 +1515,87 @@ public: // ======================= /** - * @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 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. + * @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 once per block, but it is up to the user to 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. * * @tparam T1 The type of the first index. Should be a signed or unsigned integer. * @tparam T2 The type of the index after the last index. Should be a signed or unsigned integer. - * @tparam F The type of the function to loop through. + * @tparam T The common type of the indices, as determined by `BS::common_index_type_t`. + * @tparam F The type of the block function. * @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 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 tasks 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)`. Must not return a value. * @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 -128 and +127 (a signed 8-bit integer). The default is 0. Only taken into account if the flag `BS:tp::priority` is enabled in the template parameter, otherwise has no effect. + * @param priority The priority of the tasks. Should be between -128 and +127 (a signed 8-bit integer). The default is 0. Only taken into account if the flag `BS::tp::priority` is enabled in the template parameter, otherwise has no effect. */ template , typename F> void detach_blocks(const T1 first_index, const T2 index_after_last, F&& block, const std::size_t num_blocks = 0, const priority_t priority = 0) { - if (static_cast(index_after_last) > static_cast(first_index)) + enqueue_blocks(static_cast(first_index), static_cast(index_after_last), std::forward(block), num_blocks, priority); + } + + /** + * @brief Submit an iterator range containing functions with no arguments and no return values into the task queue, with the specified priority. To submit functions with arguments, enclose them in lambda expressions. 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. + * + * @tparam I The type of the iterators. + * @param first An iterator to the first function. + * @param last An iterator to one past the last function. + * @param priority The priority of the tasks. Should be between -128 and +127 (a signed 8-bit integer). The default is 0. Only taken into account if the flag `BS::tp::priority` is enabled in the template parameter, otherwise has no effect. + */ + template + void detach_bulk(const I first, const I last, const priority_t priority = 0) + { + if (first != last) { - const std::shared_ptr> block_ptr = std::make_shared>(std::forward(block)); - const blocks blks(static_cast(first_index), static_cast(index_after_last), num_blocks ? num_blocks : thread_count); - for (std::size_t blk = 0; blk < blks.get_num_blocks(); ++blk) + bool notify = false; { - detach_task( - [block_ptr, start = blks.start(blk), end = blks.end(blk)] - { - (*block_ptr)(start, end); - }, - priority); + const std::scoped_lock tasks_lock(tasks_mutex); + if constexpr (pause_enabled) + notify = tasks.empty() && !paused; + else + notify = tasks.empty(); + for (I it = first; it != last; ++it) + { + if constexpr (priority_enabled) + tasks.emplace(std::move(*it), priority); + else + tasks.emplace(std::move(*it)); + } } + if (notify) + task_available_cv.notify_all(); } } /** - * @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 `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. + * @brief Submit a container of functions with no arguments and no return values into the task queue, with the specified priority. To submit functions with arguments, enclose them in lambda expressions. 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. + * + * @tparam C The type of the container. Must either be an array or have `begin()` and `end()` member functions. + * @param container The container. + * @param priority The priority of the tasks. Should be between -128 and +127 (a signed 8-bit integer). The default is 0. Only taken into account if the flag `BS::tp::priority` is enabled in the template parameter, otherwise has no effect. + */ + template + void detach_bulk(C& container, const priority_t priority = 0) + { + detach_bulk(std::begin(container), std::end(container), priority); + } + + /** + * @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, and it is called exactly once per index, but 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. * * @tparam T1 The type of the first index. Should be a signed or unsigned integer. * @tparam T2 The type of the index after the last index. Should be a signed or unsigned integer. - * @tparam F The type of the function to loop through. + * @tparam T The common type of the indices, as determined by `BS::common_index_type_t`. + * @tparam F The type of the loop function. * @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 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 tasks will be submitted. + * @param loop A function that will be called once per index, many times per block. Should take exactly one argument: the loop index. Must not return a value. * @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 -128 and +127 (a signed 8-bit integer). The default is 0. Only taken into account if the flag `BS:tp::priority` is enabled in the template parameter, otherwise has no effect. + * @param priority The priority of the tasks. Should be between -128 and +127 (a signed 8-bit integer). The default is 0. Only taken into account if the flag `BS::tp::priority` is enabled in the template parameter, otherwise has no effect. */ template , typename F> void detach_loop(const T1 first_index, const T2 index_after_last, F&& loop, const std::size_t num_blocks = 0, const priority_t priority = 0) { - if (static_cast(index_after_last) > static_cast(first_index)) - { - const std::shared_ptr> loop_ptr = std::make_shared>(std::forward(loop)); - const blocks blks(static_cast(first_index), static_cast(index_after_last), num_blocks ? num_blocks : thread_count); - for (std::size_t blk = 0; blk < blks.get_num_blocks(); ++blk) - { - detach_task( - [loop_ptr, start = blks.start(blk), end = blks.end(blk)] - { - for (T i = start; i < end; ++i) - (*loop_ptr)(i); - }, - priority); - } - } + enqueue_loop(static_cast(first_index), static_cast(index_after_last), std::forward(loop), num_blocks, priority); } /** @@ -1382,28 +1603,17 @@ public: * * @tparam T1 The type of the first index. Should be a signed or unsigned integer. * @tparam T2 The type of the index after the last index. Should be a signed or unsigned integer. - * @tparam F The type of the function used to define the sequence. + * @tparam T The common type of the indices, as determined by `BS::common_index_type_t`. + * @tparam F The type of the sequence function. * @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 -128 and +127 (a signed 8-bit integer). The default is 0. Only taken into account if the flag `BS:tp::priority` is enabled in the template parameter, otherwise has no effect. + * @param sequence A function that will be called once per index. Should take exactly one argument, the index. Must not return a value. + * @param priority The priority of the tasks. Should be between -128 and +127 (a signed 8-bit integer). The default is 0. Only taken into account if the flag `BS::tp::priority` is enabled in the template parameter, otherwise has no effect. */ template , typename F> void detach_sequence(const T1 first_index, const T2 index_after_last, F&& sequence, const priority_t priority = 0) { - if (static_cast(index_after_last) > static_cast(first_index)) - { - const std::shared_ptr> sequence_ptr = std::make_shared>(std::forward(sequence)); - for (T i = static_cast(first_index); i < static_cast(index_after_last); ++i) - { - detach_task( - [sequence_ptr, i] - { - (*sequence_ptr)(i); - }, - priority); - } - } + return enqueue_sequence(static_cast(first_index), static_cast(index_after_last), std::forward(sequence), priority); } /** @@ -1411,7 +1621,7 @@ public: * * @tparam F The type of the function. * @param task The function to submit. - * @param priority The priority of the task. Should be between -128 and +127 (a signed 8-bit integer). The default is 0. Only taken into account if the flag `BS:tp::priority` is enabled in the template parameter, otherwise has no effect. + * @param priority The priority of the task. Should be between -128 and +127 (a signed 8-bit integer). The default is 0. Only taken into account if the flag `BS::tp::priority` is enabled in the template parameter, otherwise has no effect. */ template void detach_task(F&& task, const priority_t priority = 0) @@ -1498,7 +1708,7 @@ public: } /** - * @brief Check whether the pool is currently paused. Only enabled if the flag `BS:tp::pause` is enabled in the template parameter. + * @brief Check whether the pool is currently paused. Only enabled if the flag `BS::tp::pause` is enabled in the template parameter. * * @return `true` if the pool is paused, `false` if it is not paused. */ @@ -1510,7 +1720,7 @@ public: } /** - * @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 the flag `BS:tp::pause` is enabled in the template parameter. + * @brief Pause the pool. The workers will temporarily stop retrieving new tasks out of the queue, although any tasks already executing will keep running until they are finished. Only enabled if the flag `BS::tp::pause` is enabled in the template parameter. */ BS_THREAD_POOL_IF_PAUSE_ENABLED void pause() @@ -1529,7 +1739,7 @@ public: } /** - * @brief 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. + * @brief Reset the pool with the default number of threads (as if constructed with the default constructor). Waits for all tasks to be completed, both running and queued, then destroys the thread pool and creates a new one with an empty task queue. If pausing is enabled, only waits for tasks that are currently running before destroying the pool; once the pool is reset, it will then resume executing the tasks that remained in the queue and any newly submitted tasks. If the pool was paused before resetting it, the new pool will be paused as well. */ void reset() { @@ -1537,7 +1747,7 @@ public: } /** - * @brief Reset the pool with a new number of threads. 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 Reset the pool with a new number of threads. Waits for all tasks to be completed, both running and queued, then destroys the thread pool and creates a new one with an empty task queue. If pausing is enabled, only waits for tasks that are currently running before destroying the pool; once the pool is reset, it will then resume executing the tasks that remained in the queue and any newly submitted tasks. If the pool was paused before resetting it, the new pool will be paused as well. * * @param num_threads The number of threads to use. */ @@ -1547,7 +1757,7 @@ public: } /** - * @brief Reset the pool with the total number of hardware threads available, as reported by the implementation, and a new initialization function. 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 and initialization function. 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 Reset the pool with the default number of threads and a new initialization function. Waits for all tasks to be completed, both running and queued, then destroys the thread pool and creates a new one with an empty task queue. If pausing is enabled, only waits for tasks that are currently running before destroying the pool; once the pool is reset, it will then resume executing the tasks that remained in the queue and any newly submitted tasks. If the pool was paused before resetting it, the new pool will be paused as well. * * @param init An initialization function to run in each thread before it starts executing any submitted tasks. The function must have no return value, and can either take one argument, the thread index of type `std::size_t`, or zero arguments. It will be executed exactly once per thread, when the thread is first constructed. The initialization function must not throw any exceptions, as that will result in program termination. Any exceptions must be handled explicitly within the function. */ @@ -1558,7 +1768,7 @@ public: } /** - * @brief Reset the pool with a new number of threads and a new initialization function. 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 and initialization function. 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 Reset the pool with a new number of threads and a new initialization function. Waits for all tasks to be completed, both running and queued, then destroys the thread pool and creates a new one with an empty task queue. If pausing is enabled, only waits for tasks that are currently running before destroying the pool; once the pool is reset, it will then resume executing the tasks that remained in the queue and any newly submitted tasks. If the pool was paused before resetting it, the new pool will be paused as well. * * @param num_threads The number of threads to use. * @param init An initialization function to run in each thread before it starts executing any submitted tasks. The function must have no return value, and can either take one argument, the thread index of type `std::size_t`, or zero arguments. It will be executed exactly once per thread, when the thread is first constructed. The initialization function must not throw any exceptions, as that will result in program termination. Any exceptions must be handled explicitly within the function. @@ -1575,6 +1785,9 @@ public: reset_pool(num_threads, std::forward(init)); tasks_lock.lock(); paused = was_paused; + tasks_lock.unlock(); + if (!was_paused) + task_available_cv.notify_all(); } else { @@ -1604,112 +1817,113 @@ public: } /** - * @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 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. + * @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 once per block, but it is up to the user to make sure the block function correctly deals with all the indices in each block. If the block function has a return value, get a `BS::multi_future` for the eventual returned values. If the block function has no return value, get a `BS::multi_future` which can be used to wait until all the tasks finish. * * @tparam T1 The type of the first index. Should be a signed or unsigned integer. * @tparam T2 The type of the index after the last index. Should be a signed or unsigned integer. - * @tparam F The type of the function to loop through. - * @tparam R The return type of the function to loop through (can be `void`). + * @tparam T The common type of the indices, as determined by `BS::common_index_type_t`. + * @tparam F The type of the block function. + * @tparam R The return type of the block function (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, and an empty `BS::multi_future` will be returned. - * @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 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 tasks will be submitted, and an empty `BS::multi_future` will be returned. + * @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)`. Can return a value. * @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 -128 and +127 (a signed 8-bit integer). The default is 0. Only taken into account if the flag `BS:tp::priority` is enabled in the template parameter, otherwise has no effect. - * @return A `BS::multi_future` that can be used to wait for all the blocks to finish. If the block function returns a value, the `BS::multi_future` can also be used to obtain the values returned by each block. + * @param priority The priority of the tasks. Should be between -128 and +127 (a signed 8-bit integer). The default is 0. Only taken into account if the flag `BS::tp::priority` is enabled in the template parameter, otherwise has no effect. + * @return A `BS::multi_future` that can be used to wait for all the tasks to finish. If the block function returns a value, the `BS::multi_future` can also be used to obtain the values returned by each block. */ template , typename F, typename R = std::invoke_result_t, T, T>> [[nodiscard]] multi_future submit_blocks(const T1 first_index, const T2 index_after_last, F&& block, const std::size_t num_blocks = 0, const priority_t priority = 0) { - if (static_cast(index_after_last) > static_cast(first_index)) + return enqueue_blocks(static_cast(first_index), static_cast(index_after_last), std::forward(block), num_blocks, priority); + } + + /** + * @brief Submit an iterator range containing functions with no arguments into the task queue, with the specified priority. To submit functions with arguments, enclose them in lambda expressions. If the functions have return values, get a `BS::multi_future` for the eventual returned values. If the functions have no return values, get a `BS::multi_future` which can be used to wait until all the tasks finish. + * + * @tparam I The type of the iterators. + * @tparam F The type of the functions. + * @tparam R The return type of the functions (can be `void`, but must be the same for all the functions). + * @param first An iterator to the first function. + * @param last An iterator to one past the last function. + * @param priority The priority of the tasks. Should be between -128 and +127 (a signed 8-bit integer). The default is 0. Only taken into account if the flag `BS::tp::priority` is enabled in the template parameter, otherwise has no effect. + * @return A `BS::multi_future` that can be used to wait for all the tasks to finish. If the functions return values, the `BS::multi_future` can also be used to obtain the values returned by each task. + */ + template ()), typename R = std::invoke_result_t>> + [[nodiscard]] multi_future submit_bulk(const I first, const I last, const priority_t priority = 0) + { + if (first != last) { - const std::shared_ptr> block_ptr = std::make_shared>(std::forward(block)); - const blocks blks(static_cast(first_index), static_cast(index_after_last), num_blocks ? num_blocks : thread_count); - multi_future future; - future.reserve(blks.get_num_blocks()); - for (std::size_t blk = 0; blk < blks.get_num_blocks(); ++blk) + const std::size_t num_tasks = static_cast(std::distance(first, last)); + multi_future all_futures; + all_futures.reserve(num_tasks); + std::vector all_tasks; + all_tasks.reserve(num_tasks); + for (I it = first; it != last; ++it) { - future.push_back(submit_task( - [block_ptr, start = blks.start(blk), end = blks.end(blk)] - { - return (*block_ptr)(start, end); - }, - priority)); + task_and_future ft(std::move(*it)); + all_futures.emplace_back(std::move(ft.future)); + all_tasks.emplace_back(std::move(ft.task)); } - return future; + detach_bulk(all_tasks, priority); + return all_futures; } return {}; } /** - * @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. It must have no return value. Returns a `BS::multi_future` that contains the futures for all of the blocks. + * @brief Submit a container of functions with no arguments into the task queue, with the specified priority. To submit functions with arguments, enclose them in lambda expressions. If the functions have return values, get a `BS::multi_future` for the eventual returned values. If the functions have no return values, get a `BS::multi_future` which can be used to wait until all the tasks finish. + * + * @tparam C The type of the container. Must either be an array or have `begin()` and `end()` member functions. + * @tparam F The type of the functions. + * @tparam R The return type of the functions (can be `void`, but must be the same for all the functions). + * @param container The container. + * @param priority The priority of the tasks. Should be between -128 and +127 (a signed 8-bit integer). The default is 0. Only taken into account if the flag `BS::tp::priority` is enabled in the template parameter, otherwise has no effect. + * @return A `BS::multi_future` that can be used to wait for all the tasks to finish. If the functions return values, the `BS::multi_future` can also be used to obtain the values returned by each task. + */ + template ().begin()), typename R = std::invoke_result_t>> + [[nodiscard]] multi_future submit_bulk(C& container, const priority_t priority = 0) + { + return submit_bulk(std::begin(container), std::end(container), priority); + } + + /** + * @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, and it is called exactly once per index, but many times per block. Returns a `BS::multi_future` which can be used to wait until all the tasks finish. * * @tparam T1 The type of the first index. Should be a signed or unsigned integer. * @tparam T2 The type of the index after the last index. Should be a signed or unsigned integer. - * @tparam F The type of the function to loop through. + * @tparam T The common type of the indices, as determined by `BS::common_index_type_t`. + * @tparam F The type of the loop function. * @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 tasks will be submitted, and an empty `BS::multi_future` will be returned. - * @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. It cannot have a return value. + * @param loop A function that will be called once per index, many times per block. Should take exactly one argument: the loop index. Must not return a value. * @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 -128 and +127 (a signed 8-bit integer). The default is 0. Only taken into account if the flag `BS:tp::priority` is enabled in the template parameter, otherwise has no effect. - * @return A `BS::multi_future` that can be used to wait for all the blocks to finish. + * @param priority The priority of the tasks. Should be between -128 and +127 (a signed 8-bit integer). The default is 0. Only taken into account if the flag `BS::tp::priority` is enabled in the template parameter, otherwise has no effect. + * @return A `BS::multi_future` that can be used to wait for all the tasks to finish. */ template , typename F> [[nodiscard]] multi_future submit_loop(const T1 first_index, const T2 index_after_last, F&& loop, const std::size_t num_blocks = 0, const priority_t priority = 0) { - if (static_cast(index_after_last) > static_cast(first_index)) - { - const std::shared_ptr> loop_ptr = std::make_shared>(std::forward(loop)); - const blocks blks(static_cast(first_index), static_cast(index_after_last), num_blocks ? num_blocks : thread_count); - multi_future future; - future.reserve(blks.get_num_blocks()); - for (std::size_t blk = 0; blk < blks.get_num_blocks(); ++blk) - { - future.push_back(submit_task( - [loop_ptr, start = blks.start(blk), end = blks.end(blk)] - { - for (T i = start; i < end; ++i) - (*loop_ptr)(i); - }, - priority)); - } - return future; - } - return {}; + return enqueue_loop(static_cast(first_index), static_cast(index_after_last), std::forward(loop), num_blocks, priority); } /** - * @brief Submit a sequence of tasks enumerated by indices to the queue, with the specified priority. The sequence function takes one argument, the task index, and will be called once per index. Returns a `BS::multi_future` that contains the futures for all of the tasks. + * @brief Submit a sequence of tasks enumerated by indices to the queue, with the specified priority. The sequence function takes one argument, the task index, and will be called once per index. If the sequence function has a return value, get a `BS::multi_future` for the eventual returned values. If the sequence function has no return value, get a `BS::multi_future` which can be used to wait until all the tasks finish. * * @tparam T1 The type of the first index. Should be a signed or unsigned integer. * @tparam T2 The type of the index after the last index. Should be a signed or unsigned integer. - * @tparam F The type of the function used to define the sequence. - * @tparam R The return type of the function used to define the sequence (can be `void`). + * @tparam T The common type of the indices, as determined by `BS::common_index_type_t`. + * @tparam F The type of the sequence function. + * @tparam R The return type of the sequence function (can be `void`). * @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, and an empty `BS::multi_future` will be returned. - * @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 -128 and +127 (a signed 8-bit integer). The default is 0. Only taken into account if the flag `BS:tp::priority` is enabled in the template parameter, otherwise has no effect. + * @param sequence A function that will be called once per index. Should take exactly one argument, the index. Can return a value. + * @param priority The priority of the tasks. Should be between -128 and +127 (a signed 8-bit integer). The default is 0. Only taken into account if the flag `BS::tp::priority` is enabled in the template parameter, otherwise has no effect. * @return A `BS::multi_future` that can be used to wait for all the tasks to finish. If the sequence function returns a value, the `BS::multi_future` can also be used to obtain the values returned by each task. */ template , typename F, typename R = std::invoke_result_t, T>> [[nodiscard]] multi_future submit_sequence(const T1 first_index, const T2 index_after_last, F&& sequence, const priority_t priority = 0) { - if (static_cast(index_after_last) > static_cast(first_index)) - { - const std::shared_ptr> sequence_ptr = std::make_shared>(std::forward(sequence)); - multi_future future; - future.reserve(static_cast(static_cast(index_after_last) > static_cast(first_index))); - for (T i = static_cast(first_index); i < static_cast(index_after_last); ++i) - { - future.push_back(submit_task( - [sequence_ptr, i] - { - return (*sequence_ptr)(i); - }, - priority)); - } - return future; - } - return {}; + return enqueue_sequence(static_cast(first_index), static_cast(index_after_last), std::forward(sequence), priority); } /** @@ -1718,56 +1932,19 @@ public: * @tparam F The type of the function. * @tparam R The return type of the function (can be `void`). * @param task The function to submit. - * @param priority The priority of the task. Should be between -128 and +127 (a signed 8-bit integer). The default is 0. Only taken into account if the flag `BS:tp::priority` is enabled in the template parameter, otherwise has no effect. + * @param priority The priority of the task. Should be between -128 and +127 (a signed 8-bit integer). The default is 0. Only taken into account if the flag `BS::tp::priority` is enabled in the template parameter, otherwise has no effect. * @return A future to be used later to wait for the function to finish executing and/or obtain its returned value if it has one. */ template >> [[nodiscard]] std::future submit_task(F&& task, const priority_t priority = 0) { -#ifdef __cpp_lib_move_only_function - std::promise promise; - #define BS_THREAD_POOL_PROMISE_MEMBER_ACCESS promise. -#else - const std::shared_ptr> promise = std::make_shared>(); - #define BS_THREAD_POOL_PROMISE_MEMBER_ACCESS promise-> -#endif - std::future future = BS_THREAD_POOL_PROMISE_MEMBER_ACCESS get_future(); - detach_task( - [task = std::forward(task), promise = std::move(promise)]() mutable - { -#ifdef __cpp_exceptions - try - { -#endif - if constexpr (std::is_void_v) - { - task(); - BS_THREAD_POOL_PROMISE_MEMBER_ACCESS set_value(); - } - else - { - BS_THREAD_POOL_PROMISE_MEMBER_ACCESS set_value(task()); - } -#ifdef __cpp_exceptions - } - catch (...) - { - try - { - BS_THREAD_POOL_PROMISE_MEMBER_ACCESS set_exception(std::current_exception()); - } - catch (...) - { - } - } -#endif - }, - priority); - return future; + task_and_future ft(std::forward(task)); + detach_task(std::move(ft.task), priority); + return std::move(ft.future); } /** - * @brief Unpause the pool. The workers will resume retrieving new tasks out of the queue. Only enabled if the flag `BS:tp::pause` is enabled in the template parameter. + * @brief Unpause the pool. The workers will resume retrieving new tasks out of the queue. Only enabled if the flag `BS::tp::pause` is enabled in the template parameter. */ BS_THREAD_POOL_IF_PAUSE_ENABLED void unpause() @@ -1782,7 +1959,7 @@ public: /** * @brief 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. * - * @throws `wait_deadlock` if called from within a thread of the same pool, which would result in a deadlock. Only enabled if the flag `BS:tp::wait_deadlock_checks` is enabled in the template parameter. + * @throws `wait_deadlock` if called from within a thread of the same pool, which would result in a deadlock. Only enabled if the flag `BS::tp::wait_deadlock_checks` is enabled in the template parameter. */ void wait() { @@ -1813,7 +1990,7 @@ public: * @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 tasks finished running, `false` if the duration expired but some tasks are still running. - * @throws `wait_deadlock` if called from within a thread of the same pool, which would result in a deadlock. Only enabled if the flag `BS:tp::wait_deadlock_checks` is enabled in the template parameter. + * @throws `wait_deadlock` if called from within a thread of the same pool, which would result in a deadlock. Only enabled if the flag `BS::tp::wait_deadlock_checks` is enabled in the template parameter. */ template bool wait_for(const std::chrono::duration& duration) @@ -1846,7 +2023,7 @@ public: * @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 tasks finished running, `false` if the time point was reached but some tasks are still running. - * @throws `wait_deadlock` if called from within a thread of the same pool, which would result in a deadlock. Only enabled if the flag `BS:tp::wait_deadlock_checks` is enabled in the template parameter. + * @throws `wait_deadlock` if called from within a thread of the same pool, which would result in a deadlock. Only enabled if the flag `BS::tp::wait_deadlock_checks` is enabled in the template parameter. */ template bool wait_until(const std::chrono::time_point& timeout_time) @@ -1943,18 +2120,129 @@ private: /** * @brief Determine how many threads the pool should have, based on the parameter passed to the constructor or reset(). * - * @param num_threads The parameter passed to the constructor or `reset()`. 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 (in which case it will have the default value of 0), then the pool will be created with the total number of hardware threads available, as obtained from `thread_t::hardware_concurrency()`. If the latter returns zero for some reason, then the pool will be created with just one thread. - * @return The number of threads to use for constructing the pool. + * @param num_threads The parameter passed to the constructor or `reset()`. If the parameter is a positive number, then the pool will be created with this number of threads. If the parameter is zero, or a parameter was not supplied (in which case it will have the default value of 0), then the pool will be created with the total number of hardware threads available, as obtained from `thread_t::hardware_concurrency()`. If the latter returns zero for some reason, then the pool will be created with just one thread. If the native extensions are enabled, the pool will instead use the number of threads available to the process, as obtained from `BS::get_os_process_affinity()`, which can be less than the number of hardware threads. */ - [[nodiscard]] static std::size_t determine_thread_count(const std::size_t num_threads) noexcept + [[nodiscard]] static std::size_t determine_thread_count(const std::size_t num_threads) noexcept(!thread_pool_native_extensions) { if (num_threads > 0) return num_threads; +#ifdef BS_THREAD_POOL_NATIVE_EXTENSIONS + const std::optional> affinity = BS::get_os_process_affinity(); + if (affinity.has_value()) + { + const std::size_t affinity_thread_count = static_cast(std::count(affinity->begin(), affinity->end(), true)); + return (affinity_thread_count > 0) ? affinity_thread_count : 1; + } +#endif if (thread_t::hardware_concurrency() > 0) return thread_t::hardware_concurrency(); return 1; } + /** + * @brief A helper function for `detach_blocks()` and `submit_blocks()`. + * + * @tparam T The type of the indices. + * @tparam F The type of the block function. + * @tparam R The return type of the block function (can be `void`). + * @tparam submit `true` if called from `submit_blocks()`, `false` if called from `detach_blocks()`. + * @tparam N The return type of this helper function. + * @param first_index The first index in the loop. + * @param index_after_last The index after the last index in the loop. + * @param block A function that will be called once per block. + * @param num_blocks The maximum number of blocks to split the loop into. + * @param priority The priority of the tasks. + * @return A `BS::multi_future` if `submit` is `true`, or `void` if `submit` is `false`. + */ + template , void>> + [[nodiscard]] N enqueue_blocks(const T first_index, const T index_after_last, F&& block, std::size_t num_blocks, const priority_t priority = 0) + { + if (index_after_last > first_index) + { + using block_task_t = block_task; + const std::shared_ptr> block_ptr = std::make_shared>(std::forward(block)); + const blocks blks(first_index, index_after_last, num_blocks ? num_blocks : thread_count); + num_blocks = blks.get_num_blocks(); + std::vector> all_tasks; + all_tasks.reserve(num_blocks); + for (std::size_t i = 0; i < num_blocks; ++i) + all_tasks.emplace_back(block_task_t{block_ptr, blks.start(i), blks.end(i)}); + if constexpr (submit) + return submit_bulk(all_tasks, priority); + else + detach_bulk(all_tasks, priority); + } + return N(); + } + + /** + * @brief A helper function for `detach_loop()` and `submit_loop()`. + * + * @tparam T The type of the indices. + * @tparam F The type of the loop function. + * @tparam submit `true` if called from `submit_loop()`, `false` if called from `detach_loop()`. + * @tparam N The return type of this helper function. + * @param first_index The first index in the loop. + * @param index_after_last The index after the last index in the loop. + * @param loop A function that will be called once per index, many times per block. + * @param num_blocks The maximum number of blocks to split the loop into. + * @param priority The priority of the tasks. + * @return A `BS::multi_future` if `submit` is `true`, or `void` if `submit` is `false`. + */ + template , void>> + [[nodiscard]] N enqueue_loop(const T first_index, const T index_after_last, F&& loop, std::size_t num_blocks, const priority_t priority = 0) + { + if (index_after_last > first_index) + { + using loop_task_t = loop_task; + const std::shared_ptr> loop_ptr = std::make_shared>(std::forward(loop)); + const blocks blks(first_index, index_after_last, num_blocks ? num_blocks : thread_count); + num_blocks = blks.get_num_blocks(); + std::vector> all_tasks; + all_tasks.reserve(num_blocks); + for (std::size_t i = 0; i < num_blocks; ++i) + all_tasks.emplace_back(loop_task_t{loop_ptr, blks.start(i), blks.end(i)}); + if constexpr (submit) + return submit_bulk(all_tasks, priority); + else + detach_bulk(all_tasks, priority); + } + return N(); + } + + /** + * @brief A helper function for `detach_sequence()` and `submit_sequence()`. + * + * @tparam T The type of the indices. + * @tparam F The type of the sequence function. + * @tparam R The return type of the sequence function (can be `void`). + * @tparam submit `true` if called from `submit_sequence()`, `false` if called from `detach_sequence()`. + * @tparam N The return type of this helper function. + * @param first_index The first index in the sequence. + * @param index_after_last The index after the last index in the sequence. + * @param sequence A function that will be called once per index. + * @param priority The priority of the tasks. + * @return A `BS::multi_future` if `submit` is `true`, or `void` if `submit` is `false`. + */ + template , void>> + [[nodiscard]] N enqueue_sequence(const T first_index, const T index_after_last, F&& sequence, const priority_t priority = 0) + { + if (index_after_last > first_index) + { + using sequence_task_t = sequence_task; + const std::shared_ptr> sequence_ptr = std::make_shared>(std::forward(sequence)); + std::vector> all_tasks; + all_tasks.reserve(static_cast(index_after_last - first_index)); + for (T i = first_index; i < index_after_last; ++i) + all_tasks.emplace_back(sequence_task_t{sequence_ptr, i}); + if constexpr (submit) + return submit_bulk(all_tasks, priority); + else + detach_bulk(all_tasks, priority); + } + return N(); + } + /** * @brief Pop a task from the queue. * @@ -1964,7 +2252,7 @@ private: { task_t task; if constexpr (priority_enabled) - task = std::move(const_cast(tasks.top()).task); + task = std::move(tasks.top().task); else task = std::move(tasks.front()); tasks.pop(); @@ -2048,19 +2336,9 @@ private: // ============ /** - * @brief A cleanup function to run in each thread right before it is destroyed, which will happen when the pool is destructed or reset. The function must have no return value, and can either take one argument, the thread index of type `std::size_t`, or zero arguments. The cleanup function must not throw any exceptions, as that will result in program termination. Any exceptions must be handled explicitly within the function. The default is an empty function, i.e., no cleanup will be performed. + * @brief A mutex to synchronize access to the task queue by different threads. */ - function_t cleanup_func = [](std::size_t) {}; - - /** - * @brief An initialization function to run in each thread before it starts executing any submitted tasks. The function must have no return value, and can either take one argument, the thread index of type `std::size_t`, or zero arguments. It will be executed exactly once per thread, when the thread is first constructed. The initialization function must not throw any exceptions, as that will result in program termination. Any exceptions must be handled explicitly within the function. The default is an empty function, i.e., no initialization will be performed. - */ - function_t init_func = [](std::size_t) {}; - - /** - * @brief A flag indicating whether the workers should pause. When set to `true`, the workers temporarily stop retrieving new tasks out of the queue, although any tasks already executed will keep running until they are finished. When set to `false` again, the workers resume retrieving tasks. Only enabled if the flag `BS:tp::pause` is enabled in the template parameter. - */ - std::conditional_t paused = {}; + mutable std::mutex tasks_mutex; /** * @brief A condition variable to notify `worker()` that a new task has become available. @@ -2077,16 +2355,21 @@ private: */ std::condition_variable tasks_done_cv; + /** + * @brief A cleanup function to run in each thread right before it is destroyed, which will happen when the pool is destructed or reset. The function must have no return value, and can either take one argument, the thread index of type `std::size_t`, or zero arguments. The cleanup function must not throw any exceptions, as that will result in program termination. Any exceptions must be handled explicitly within the function. The default is an empty function, i.e., no cleanup will be performed. + */ + move_only_function cleanup_func = [](std::size_t) {}; + + /** + * @brief An initialization function to run in each thread before it starts executing any submitted tasks. The function must have no return value, and can either take one argument, the thread index of type `std::size_t`, or zero arguments. It will be executed exactly once per thread, when the thread is first constructed. The initialization function must not throw any exceptions, as that will result in program termination. Any exceptions must be handled explicitly within the function. The default is an empty function, i.e., no initialization will be performed. + */ + move_only_function init_func = [](std::size_t) {}; + /** * @brief A queue of tasks to be executed by the threads. */ std::conditional_t, std::queue> tasks; - /** - * @brief A mutex to synchronize access to the task queue by different threads. - */ - mutable std::mutex tasks_mutex; - /** * @brief A counter for the total number of currently running tasks. */ @@ -2102,6 +2385,11 @@ private: */ std::unique_ptr threads = nullptr; + /** + * @brief A flag indicating whether the workers should pause. When set to `true`, the workers temporarily stop retrieving new tasks out of the queue, although any tasks already executing will keep running until they are finished. When set to `false` again, the workers resume retrieving tasks. Only enabled if the flag `BS::tp::pause` is enabled in the template parameter. + */ + std::conditional_t paused = {}; + /** * @brief A flag indicating that `wait()` is active and expects to be notified whenever a task is done. */ @@ -2116,7 +2404,7 @@ private: }; // class thread_pool /** - * @brief A utility class to synchronize printing to an output stream by different threads. + * @brief A utility class to synchronize printing to one or more output streams by different threads. */ class [[nodiscard]] synced_stream { @@ -2162,7 +2450,7 @@ public: } /** - * @brief Print any number of items into the output stream. Ensures that no other threads print to this stream simultaneously, as long as they all exclusively use the same `BS::synced_stream` object to print. + * @brief Print any number of items into each output stream. Ensures that no other threads print to the streams simultaneously, as long as they all exclusively use the same `BS::synced_stream` object to print. * * @tparam T The types of the items. * @param items The items to print. @@ -2176,7 +2464,7 @@ public: } /** - * @brief Print any number of items into the output stream, followed by a newline character. Ensures that no other threads print to this stream simultaneously, as long as they all exclusively use the same `BS::synced_stream` object to print. + * @brief Print any number of items into each output stream, followed by a newline character. Ensures that no other threads print to the streams simultaneously, as long as they all exclusively use the same `BS::synced_stream` object to print. * * @tparam T The types of the items. * @param items The items to print. @@ -2208,166 +2496,15 @@ public: inline static std::ostream& (&flush)(std::ostream&) = static_cast(std::flush); private: - /** - * @brief The output streams to print to. - */ - std::vector out_streams; - /** * @brief A mutex to synchronize printing. */ mutable std::mutex stream_mutex; + + /** + * @brief The output streams to print to. + */ + std::vector out_streams; }; // class synced_stream - -#ifdef __cpp_lib_semaphore -using binary_semaphore = std::binary_semaphore; -template ::max()> -using counting_semaphore = std::counting_semaphore; -#else -/** - * @brief A polyfill for `std::counting_semaphore`, to be used if C++20 features are not available. A `counting_semaphore` is a synchronization primitive that allows more than one concurrent access to the same resource. The number of concurrent accessors is limited by the semaphore's counter, which is decremented when a thread acquires the semaphore and incremented when a thread releases the semaphore. If the counter is zero, a thread trying to acquire the semaphore will be blocked until another thread releases the semaphore. - * - * @tparam LeastMaxValue The least maximum value of the counter. (In this implementation, it is also the actual maximum value.) - */ -template ::max()> -class [[nodiscard]] counting_semaphore -{ - static_assert(LeastMaxValue >= 0, "The least maximum value for a counting semaphore must not be negative."); - -public: - /** - * @brief Construct a new counting semaphore with the given initial counter value. - * - * @param desired The initial counter value. - */ - constexpr explicit counting_semaphore(const std::ptrdiff_t desired) : counter(desired) {} - - // The copy and move constructors and assignment operators are deleted. The semaphore cannot be copied or moved. - counting_semaphore(const counting_semaphore&) = delete; - counting_semaphore(counting_semaphore&&) = delete; - counting_semaphore& operator=(const counting_semaphore&) = delete; - counting_semaphore& operator=(counting_semaphore&&) = delete; - ~counting_semaphore() = default; - - /** - * @brief Returns the internal counter's maximum possible value, which in this implementation is equal to `LeastMaxValue`. - * - * @return The internal counter's maximum possible value. - */ - [[nodiscard]] static constexpr std::ptrdiff_t max() noexcept - { - return LeastMaxValue; - } - - /** - * @brief Atomically decrements the internal counter by 1 if it is greater than 0; otherwise blocks until it is greater than 0 and can successfully decrement the internal counter. - */ - void acquire() - { - std::unique_lock lock(mutex); - cv.wait(lock, - [this] - { - return counter > 0; - }); - --counter; - } - - /** - * @brief Atomically increments the internal counter. Any thread(s) waiting for the counter to be greater than 0, such as due to being blocked in `acquire()`, will subsequently be unblocked. - * - * @param update The amount to increment the internal counter by. Defaults to 1. - */ - void release(const std::ptrdiff_t update = 1) - { - { - const std::scoped_lock lock(mutex); - counter += update; - } - cv.notify_all(); - } - - /** - * @brief Tries to atomically decrement the internal counter by 1 if it is greater than 0; no blocking occurs regardless. - * - * @return `true` if decremented the internal counter, `false` otherwise. - */ - bool try_acquire() - { - std::scoped_lock lock(mutex); - if (counter > 0) - { - --counter; - return true; - } - return false; - } - - /** - * @brief Tries to atomically decrement the internal counter by 1 if it is greater than 0; otherwise blocks until it is greater than 0 and can successfully decrement the internal counter, or the `rel_time` duration has been exceeded. - * - * @tparam Rep An arithmetic type representing the number of ticks to wait. - * @tparam Period An `std::ratio` representing the length of each tick in seconds. - * @param rel_time The duration the function must wait. Note that the function may wait for longer. - * @return `true` if decremented the internal counter, `false` otherwise. - */ - template - bool try_acquire_for(const std::chrono::duration& rel_time) - { - std::unique_lock lock(mutex); - if (!cv.wait_for(lock, rel_time, - [this] - { - return counter > 0; - })) - return false; - --counter; - return true; - } - - /** - * @brief Tries to atomically decrement the internal counter by 1 if it is greater than 0; otherwise blocks until it is greater than 0 and can successfully decrement the internal counter, or the `abs_time` time point has been passed. - * - * @tparam Clock The type of the clock used to measure time. - * @tparam Duration An `std::chrono::duration` type used to indicate the time point. - * @param abs_time The earliest time the function must wait until. Note that the function may wait for longer. - * @return `true` if decremented the internal counter, `false` otherwise. - */ - template - bool try_acquire_until(const std::chrono::time_point& abs_time) - { - std::unique_lock lock(mutex); - if (!cv.wait_until(lock, abs_time, - [this] - { - return counter > 0; - })) - return false; - --counter; - return true; - } - -private: - /** - * @brief The semaphore's counter. - */ - std::ptrdiff_t counter; - - /** - * @brief A condition variable used to wait for the counter. - */ - std::condition_variable cv; - - /** - * @brief A mutex used to synchronize access to the counter. - */ - mutable std::mutex mutex; -}; - -/** - * @brief A polyfill for `std::binary_semaphore`, to be used if C++20 features are not available. - */ -using binary_semaphore = counting_semaphore<1>; -#endif } // namespace BS #endif // BS_THREAD_POOL_HPP diff --git a/modules/BS.thread_pool.cppm b/modules/BS.thread_pool.cppm index 1951240..6985bed 100644 --- a/modules/BS.thread_pool.cppm +++ b/modules/BS.thread_pool.cppm @@ -7,9 +7,9 @@ * * @file BS.thread_pool.cppm * @author Barak Shoshany (baraksh@gmail.com) (https://baraksh.com/) - * @version 5.0.0 - * @date 2024-12-19 - * @copyright Copyright (c) 2024 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.1016/j.softx.2024.101687, SoftwareX 26 (2024) 101687, arXiv:2105.00613 + * @version 5.1.0 + * @date 2026-01-03 + * @copyright Copyright (c) 2021-2026 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.1016/j.softx.2024.101687, SoftwareX 26 (2024) 101687, arXiv:2105.00613 * * @brief `BS::thread_pool`: a fast, lightweight, modern, and easy-to-use C++17/C++20/C++23 thread pool library. This module file wraps the header file BS_thread_pool.hpp inside a C++20 module so it can be imported using `import BS.thread_pool`. */ @@ -17,18 +17,20 @@ module; // A macro indicating to the library that it is being imported as a module, as well as the version of the module file, which must match the version of the header file. -#define BS_THREAD_POOL_MODULE 5, 0, 0 +#define BS_THREAD_POOL_MODULE 5, 1, 0 #include "BS_thread_pool.hpp" export module BS.thread_pool; export namespace BS { -using BS::binary_semaphore; using BS::common_index_type_t; -using BS::counting_semaphore; using BS::light_thread_pool; using BS::multi_future; +using BS::operator&; +using BS::operator^; +using BS::operator|; +using BS::operator~; using BS::pause_thread_pool; using BS::pr; using BS::priority_t; @@ -42,9 +44,12 @@ using BS::thread_pool_native_extensions; using BS::thread_pool_version; using BS::tp; using BS::version; -using BS::wait_deadlock; using BS::wdc_thread_pool; +#ifdef __cpp_exceptions +using BS::wait_deadlock; +#endif + #ifdef BS_THREAD_POOL_NATIVE_EXTENSIONS using BS::get_os_process_affinity; using BS::get_os_process_priority; diff --git a/pyproject.toml b/pyproject.toml index 8b8a899..7054fc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -134,6 +134,7 @@ ignore = [ "S603", "S607", "SIM108", + "SIM112", "SLF001", "T201", "TCH003", @@ -168,7 +169,6 @@ prefer-stubs = true py-version = "3.13" recursive = false source-roots = [] -suggestion-mode = true unsafe-load-any-extension = false [tool.pylint.basic] diff --git a/scripts/clear_folder.py b/scripts/clear_folder.py deleted file mode 100644 index fed58ab..0000000 --- a/scripts/clear_folder.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -██████ ███████ ████████ ██ ██ ██████ ███████ █████ ██████ ██████ ██████ ██████ ██ -██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -██████ ███████ ██ ███████ ██████ █████ ███████ ██ ██ ██████ ██ ██ ██ ██ ██ -██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -██████ ███████ ██ ██ ██ ██ ██ ███████ ██ ██ ██████ ███████ ██ ██████ ██████ ███████ - -`BS::thread_pool`: a fast, lightweight, modern, and easy-to-use C++17/C++20/C++23 thread pool library -v5.0.0 (2024-12-19) -By Barak Shoshany -Copyright (c) 2024 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.1016/j.softx.2024.101687, SoftwareX 26 (2024) 101687, arXiv:2105.00613 - -This Python script is used in the library's development environment to clear all files a folder. It is not part of the library itself. -""" - -import argparse -import importlib -import importlib.util -import pathlib -import sys -from importlib.machinery import ModuleSpec -from types import ModuleType - -send2trash_spec: ModuleSpec | None = importlib.util.find_spec("send2trash") -if send2trash_spec is not None: - # Install with `pip install send2trash`. - send2trash: ModuleType = importlib.import_module("send2trash") - - def delete_files_in_folder(folder_path: pathlib.Path) -> None: - """Recursively delete all files in a folder.""" - for child in folder_path.iterdir(): - if child.is_file(): - send2trash.send2trash(paths=child) - else: - delete_files_in_folder(child) - send2trash.send2trash(folder_path) -else: - print("Note: Module send2trash not found; deleting files permanently.") - - def delete_files_in_folder(folder_path: pathlib.Path) -> None: - """Fallback function in case `send2trash` is not installed.""" - for child in folder_path.iterdir(): - if child.is_file(): - child.unlink() - else: - delete_files_in_folder(child) - folder_path.rmdir() - - -parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) -_ = parser.add_argument("folder", help="the folder to clear (relative to this script's location)") -parsed_args: argparse.Namespace = parser.parse_args(args=None if len(sys.argv) > 1 else ["--help"]) - -folder: pathlib.Path = (pathlib.Path(__file__).parent / parsed_args.folder).resolve() -if not folder.exists(): - print(f"Folder {folder} does not exist; creating empty.") -else: - try: - delete_files_in_folder(folder) - folder.mkdir() - print(f"Folder {folder} successfully cleared.") - except Exception as exc: - sys.exit(f"Error clearing folder {folder}: {exc}") diff --git a/scripts/compile_cpp.py b/scripts/compile_cpp.py index 31311df..86cdbf3 100644 --- a/scripts/compile_cpp.py +++ b/scripts/compile_cpp.py @@ -6,9 +6,9 @@ ██████ ███████ ██ ██ ██ ██ ██ ███████ ██ ██ ██████ ███████ ██ ██████ ██████ ███████ `BS::thread_pool`: a fast, lightweight, modern, and easy-to-use C++17/C++20/C++23 thread pool library -v5.0.0 (2024-12-19) +v5.1.0 (2026-01-03) By Barak Shoshany -Copyright (c) 2024 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.1016/j.softx.2024.101687, SoftwareX 26 (2024) 101687, arXiv:2105.00613 +Copyright (c) 2021-2026 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.1016/j.softx.2024.101687, SoftwareX 26 (2024) 101687, arXiv:2105.00613 This Python script can be used to compile simple C++ programs (with only a few source and/or header files) using a variety of compilers, C++ standards, and other options. It also includes support for C++20 modules and C++23 Standard Library modules. It is used in the thread pool library's development environment to compile and run the test program using different compilers and C++ standards. It is not part of the library itself, but users of the library may find it useful, especially if they wish to use the library as a C++20 module. """ @@ -17,15 +17,16 @@ import argparse import os import pathlib import platform +import re import shutil import subprocess import sys import time +from enum import Enum +from typing import Any, Never, cast import yaml # Install with `pip install pyyaml`. -separator: str = "=" * 60 - class Args: """A class to collect the command line arguments with proper type checking.""" @@ -35,10 +36,14 @@ class Args: self.files: list[str] = parsed_ns.files self.arch: str = parsed_ns.arch self.as_module: bool = parsed_ns.as_module + self.clear_output: bool = parsed_ns.clear_output self.compiler: str | None = parsed_ns.compiler self.define: list[str] = parsed_ns.define if parsed_ns.define is not None else [] + self.deps: list[str] = parsed_ns.deps if parsed_ns.deps is not None else [] + self.disable_exceptions: str | None = parsed_ns.disable_exceptions self.flag: list[str] = parsed_ns.flag if parsed_ns.flag is not None else [] - self.ignore_yaml: bool = parsed_ns.ignore_yaml + self.force: bool = parsed_ns.force + self.ignore_config: bool = parsed_ns.ignore_config self.include: list[str] = parsed_ns.include if parsed_ns.include is not None else [] self.module: list[str] = parsed_ns.module if parsed_ns.module is not None else [] self.output: str | None = parsed_ns.output @@ -46,81 +51,346 @@ class Args: self.run: bool = parsed_ns.run self.std_module: str | None = parsed_ns.std_module self.std: str = parsed_ns.std + self.try_all: bool = parsed_ns.try_all self.type: str = parsed_ns.type self.verbose: bool = parsed_ns.verbose # Parse the command-line arguments. parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) -_ = parser.add_argument("files", nargs="+", help="the source file(s) to compile") -_ = parser.add_argument("-a", "--arch", choices=["amd64", "arm64"], default="amd64", help="the target architecture (MSVC only)") -_ = parser.add_argument("-c", "--compiler", choices=["cl", "clang++", "g++"], help="which compiler to use (auto determined if not specified)") +_ = parser.add_argument("files", action="store", nargs="*", help="the source file(s) to compile") +_ = parser.add_argument("-a", "--arch", action="store", choices=["amd64", "arm64"], default="amd64", help="the target architecture (MSVC only)") +_ = parser.add_argument("-b", "--clear-output", action="store_true", help="clear the output folder before compiling (if no source files are specified, just clear and exit)") +_ = parser.add_argument("-c", "--compiler", action="store", choices=["cl", "clang++", "g++"], help="which compiler to use (auto determined if not specified)") _ = parser.add_argument("-d", "--define", action="append", help="macros to define (use multiple times if more than one) [in addition to those in compile_cpp.yaml]") +_ = parser.add_argument("-e", "--force", action="store_true", help="force recompilation even if the compiled file is up to date") _ = parser.add_argument("-f", "--flag", action="append", help="extra compiler flags to add (use multiple times if more than one) [in addition to those in compile_cpp.yaml]") -_ = parser.add_argument("-g", "--ignore-yaml", action="store_true", help="ignore the compile_cpp.yaml file") +_ = parser.add_argument("-g", "--ignore-config", action="store_true", help="ignore the compile_cpp.yaml configuration file") _ = parser.add_argument("-i", "--include", action="append", help="the include folder to use (use multiple times if more than one) [in addition to those in compile_cpp.yaml]") -_ = parser.add_argument("-l", "--as-module", action="store_true", help="Compile file as module") -_ = parser.add_argument("-m", "--module", action="append", help='C++20 module files to use if desired, in the format "module_name=module_file,dependent_files,..." (use multiple times if more than one) [in addition to those in compile_cpp.yaml]') -_ = parser.add_argument("-o", "--output", help="the output folder (end with / to create, taken from compile_cpp.yaml if not specified) and/or executable name (auto determined if not specified)") -_ = parser.add_argument("-p", "--pass", action="append", dest="pass_args", help="pass command line arguments to the compiled program when running it, if -r is specified (use multiple times if more than one) [in addition to those in compile_cpp.yaml]") +_ = parser.add_argument("-l", "--as-module", action="store_true", help="compile file as module") +_ = parser.add_argument("-m", "--module", action="append", help='C++20 module files to use if desired, in the format "module_name=module_file,dependencies,..." (use multiple times if more than one) [in addition to those in compile_cpp.yaml]') +_ = parser.add_argument("-n", "--deps", action="append", help="dependencies used to detect if recompilation is needed (use multiple times if more than one) [in addition to modules and those in compile_cpp.yaml]") +_ = parser.add_argument("-o", "--output", action="store", help="the output folder (end with / to create, taken from compile_cpp.yaml if not specified) and/or binary name (auto determined if not specified)") +_ = parser.add_argument("-p", "--pass", action="append", dest="pass_args", help="pass command line arguments to the compiled program when running it, if -r/--run is specified (use multiple times if more than one) [in addition to those in compile_cpp.yaml]") _ = parser.add_argument("-r", "--run", action="store_true", help="run the program after compiling it") -_ = parser.add_argument("-s", "--std", choices=["c++17", "c++20", "c++23"], default="c++23", help="which C++ standard to use") -_ = parser.add_argument("-t", "--type", choices=["debug", "release"], default="debug", help="whether to compile in debug or release mode") -_ = parser.add_argument("-u", "--std-module", help="path to the standard library module (C++23 only, taken from compile_cpp.yaml if not specified, use 'auto' to auto-detect, 'disable' to explicitly disable)") +_ = parser.add_argument("-s", "--std", action="store", choices=["c++17", "c++20", "c++23"], default="c++23", help="which C++ standard to use") +_ = parser.add_argument("-t", "--type", action="store", choices=["debug", "release"], default="debug", help="whether to compile in debug or release mode") +_ = parser.add_argument("-u", "--std-module", action="store", help="path to the standard library module (C++23 only, taken from compile_cpp.yaml if not specified, use 'auto' to auto-detect, 'disable' to explicitly disable)") _ = parser.add_argument("-v", "--verbose", action="store_true", help="whether to print this script's diagnostic messages") +_ = parser.add_argument("-x", "--disable-exceptions", action="store", choices=["true", "false"], help="whether to disable exceptions [overrides compile_cpp.yaml]") +_ = parser.add_argument("-y", "--try-all", action="store_true", help="test compilation using all possible combinations of available compilers and C++ standards (also runs each compiled program if -r/--run is specified)") args = Args(parser.parse_args(args=None if len(sys.argv) > 1 else ["--help"])) -def print_if_verbose(message: str) -> None: - """Print a message, but only if the verbose flag is set.""" +class ANSI(Enum): + """Enumeration of ANSI color codes for terminal output.""" + + reset = 0 + bold = 1 + dim = 2 + italic = 3 + underline = 4 + invert = 7 + strike = 9 + double_underline = 21 + fg_black = 30 + fg_red = 31 + fg_green = 32 + fg_yellow = 33 + fg_blue = 34 + fg_magenta = 35 + fg_cyan = 36 + fg_white = 37 + bg_black = 40 + bg_red = 41 + bg_green = 42 + bg_yellow = 43 + bg_blue = 44 + bg_magenta = 45 + bg_cyan = 46 + bg_white = 47 + fg_bright_black = 90 + fg_bright_red = 91 + fg_bright_green = 92 + fg_bright_yellow = 93 + fg_bright_blue = 94 + fg_bright_magenta = 95 + fg_bright_cyan = 96 + fg_bright_white = 97 + bg_bright_black = 100 + bg_bright_red = 101 + bg_bright_green = 102 + bg_bright_yellow = 103 + bg_bright_blue = 104 + bg_bright_magenta = 105 + bg_bright_cyan = 106 + bg_bright_white = 107 + + +ANSI_INFO = ANSI.fg_bright_blue +ANSI_ERROR = ANSI.fg_bright_red +ANSI_SUCCESS = ANSI.fg_bright_green +ANSI_SEPARATOR = ANSI.fg_bright_yellow +ANSI_TRY_ALL = ANSI.fg_bright_magenta + + +no_color: bool = os.environ.get("NO_COLOR", "") != "" + + +def print_ansi(message: str, *codes: ANSI) -> None: + """Print a message. Uses the given ANSI codes if specified. Does not add newline. Does not print color if the `NO_COLOR` environment variable is set.""" + if len(codes) > 0 and not no_color: + seq = ";".join(str(c.value) for c in codes) + print(f"\033[{seq}m{message}\033[{ANSI.reset.value}m", end="") + else: + print(message, end="") + + +def print_if_verbose(message: str, *codes: ANSI) -> None: + """Print a message followed by a newline, but only if the verbose flag is set. Uses the given ANSI codes if specified.""" if args.verbose: - print(message) + print_ansi(message + "\n", *codes) + + +def print_separator() -> None: + """Print a separator line, but only if the verbose flag is set.""" + print_if_verbose("=" * 60, ANSI_SEPARATOR) + + +def print_key_values(key: str, values: list[str], length: int) -> None: + """Print a key-value pair with the key left-aligned to the given length, but only if the verbose flag is set.""" + if args.verbose: + print_ansi((key + ":").ljust(length), ANSI.fg_bright_white, ANSI.bold) + if len(values) == 0: + print("") + else: + print(values[0]) + for value in values[1:]: + print(" " * length + f"{value}") + + +def print_error_and_exit(message: str) -> Never: + """Print an error message followed by a newline in red and exit.""" + print_ansi(message + "\n", ANSI_ERROR) + sys.exit(1) + + +def relative_or_full_path(path: pathlib.Path) -> pathlib.Path: + """If the path is relative to the current working directory, return it as a relative path; otherwise, return it as a full path.""" + return path.relative_to(pathlib.Path.cwd()) if path.is_relative_to(pathlib.Path.cwd()) else path + + +def parse_llvm_version(path_str: str) -> tuple[int, ...] | None: + """Extract the LLVM version from a path and returns it as a tuple of integers.""" + match: re.Match[str] | None = re.search(r"/llvm[-/](\d+(?:\.\d+)*)", path_str) + if match is None: + return None + return tuple(int(part) for part in match.group(1).split(".")) + + +def get_llvm_std_module(search: str) -> str | None: + """Get the path to the LLVM standard library module on Linux or macOS. If multiple paths are found, return the one with the latest LLVM version.""" + try: + llvm_path: str = subprocess.check_output([f"find {search} -name std.cppm"], text=True, shell=True).strip() + except subprocess.CalledProcessError: + llvm_path: str = "" + all_paths: list[str] = [line for line in llvm_path.splitlines() if line.strip()] + if len(all_paths) == 0: + return None + return max(all_paths, key=lambda p: parse_llvm_version(p) or (0,)) + + +def find_vs_path() -> pathlib.Path | None: + """Find the Visual Studio installation path, if it exists.""" + if platform.system() != "Windows": + return None + pf86: str = os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)") + pf: str = os.environ.get("ProgramFiles", r"C:\Program Files") + try_paths: list[pathlib.Path] = [pathlib.Path(p) / "Microsoft Visual Studio" / "Installer" / "vswhere.exe" for p in [pf86, pf]] + vswhere: pathlib.Path | None = next((p for p in try_paths if p.exists()), None) + if vswhere is None: + return None + try: + install_root: str = subprocess.check_output( + [ + str(vswhere), + "-latest", + "-property", + "installationPath", + ], + text=True, + ).strip() + except subprocess.CalledProcessError: + return None + if install_root == "": + return None + return pathlib.Path(install_root) + + +def get_module_flags(name: str, module_output_path: pathlib.Path) -> list[str]: + """Get the appropriate flags to import a module with the given name from the given output path.""" + if compiler == "cl": + return ["/reference", f"{name}={module_output_path.resolve()}", *([str(build_folder / f"{module_output_path.stem}.obj")] if (name != "std" or not args.as_module) else [])] + if compiler == "clang++": + return [f"-fmodule-file={name}={module_output_path.resolve()}"] + # Otherwise, compiler == "g++". + return ["-fmodules", f"-fmodule-mapper=|@g++-mapper-server -r{build_folder}"] + + +def compile_module(name: str, paths: list[pathlib.Path], module_output_path: pathlib.Path) -> bool: + """Compile a module with the given name. The first path in `paths` is the module file, and the rest are its dependencies. Returns True if the module was recompiled, False if it was up to date.""" + module_file: pathlib.Path = paths[0] + if module_output_path.exists(): + module_output_mod: float = module_output_path.stat().st_mtime + if not any((path.exists() and path.stat().st_mtime > module_output_mod) for path in paths): + print_if_verbose(f'Module "{name}" is up to date, skipping compilation.', ANSI_INFO) + return False + try: + module_command: list[str] = [ + sys.executable, + str(pathlib.Path(__file__).resolve()), + # In the special case of GCC and the `std` module, `bits/std.cc` must be entered without the full path. + str(module_file) if not (compiler == "g++" and name == "std") else gcc_std_path, + f"--output={module_output_path.resolve()}", + f"--arch={args.arch}", + f"--compiler={compiler}", + f"--std={args.std}", + f"--type={args.type}", + # Note: Not adding the extra options from the configuration file, since they will be added by the script anyway. + *[f"--define={define}" for define in args.define], + *[f"--include={include}" for include in args.include], + *[f"--flag={flag}" for flag in args.flag], + # For the `std` module in GCC, we also need to add `-fsearch-include-path`. + *(["--flag=-fsearch-include-path"] if (compiler == "g++" and name == "std") else []), + # If compiling the `std` module itself, we need to pass `-u=disable` to avoid infinite recursion. Otherwise, we pass along the specified module path if it exists. + *(["--std-module=disable"] if name == "std" else [f"--std-module={std_module}"] if std_module is not None else []), + "--as-module", + *(["--verbose"] if args.verbose else []), + *(["--disable-exceptions=true"] if disable_exceptions else ["--disable-exceptions=false"]), + ] + print_separator() + print_if_verbose(f'Compiling module "{name}" with command: {subprocess.list2cmdline(module_command)}', ANSI_INFO) + _ = sys.stdout.flush() + module_result = subprocess.run( + args=module_command, + check=False, + text=True, + ) + if module_result.returncode != 0: + print_error_and_exit(f"Module compilation failed with return code: {module_result.returncode}.") + except Exception as exc: + print_error_and_exit(f"Could not compile module due to exception: {exc}.") + else: + return True # Collect the full path(s) to the source file(s). -source_paths: list[pathlib.Path] = [pathlib.Path(file).resolve() for file in args.files] +source_paths: list[pathlib.Path] = [pathlib.Path(file) for file in args.files] + +# If the `try_all` flag is set, run this script recursively using all possible combinations of compilers and C++ standards available in the system. +if args.try_all: + if "-c" in sys.argv or "--compiler" in sys.argv: + print_error_and_exit("Error: The -y/--try-all flag cannot be used together with the -c/--compiler flag.") + if "-s" in sys.argv or "--std" in sys.argv: + print_error_and_exit("Error: The -y/--try-all flag cannot be used together with the -s/--std flag.") + # We pass all the arguments to the child processes except for the one that enables this mode, to avoid infinite recursion. + child_args: list[str] = [arg for arg in sys.argv[1:] if arg not in ("-y", "--try-all")] + + # Determine which compilers are available. + compilers: list[str] = [] + if find_vs_path() is not None: + compilers.append("cl") + if shutil.which("clang++") is not None: + compilers.append("clang++") + # On macOS, g++ is by default just an alias for clang++, so we skip it. + if shutil.which("g++") is not None and platform.system() != "Darwin": + compilers.append("g++") + + # Compile using all available compilers using all relevant C++ standards. + standards: list[str] = ["c++17", "c++20", "c++23"] + workspace_path: pathlib.Path = pathlib.Path(__file__).parent.parent.resolve() + try: + compile_start: float = time.perf_counter() + for compiler in compilers: + for std in standards: + warnings_as_errors: list[str] = ["-f/WX"] if compiler == "cl" else ["-f-Werror"] + command: list[str] = [ + sys.executable, + str(pathlib.Path(__file__).resolve()), + "-c", + compiler, + "-s", + std, + *warnings_as_errors, + *child_args, + ] + print_if_verbose(f"Compiling with {compiler} using {std.upper()} standard with command: {subprocess.list2cmdline(command)}", ANSI_TRY_ALL) + compile_result = subprocess.run( + args=command, + check=False, + text=True, + ) + if compile_result.returncode != 0: + print_if_verbose("Compilation failed, aborting!", ANSI_TRY_ALL) + sys.exit(1) + except Exception as exc: + print_if_verbose(f"Could not compile due to exception: {exc}, aborting!", ANSI_TRY_ALL) + sys.exit(1) + print_if_verbose(f"All compilations completed successfully in {time.perf_counter() - compile_start:.2f} seconds.", ANSI_TRY_ALL) + sys.exit(0) # Determine the compiler if it is not given. compiler: str = "" -vs_pwsh_path: str = r"C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\Launch-VsDevShell.ps1" +vs_path: pathlib.Path | None = find_vs_path() if args.compiler is not None: compiler = args.compiler -else: # noqa: PLR5501 - # On Windows, we default to MSVC if the Visual Studio Developer PowerShell script exists, otherwise we fall back to Clang, and then GCC. - if platform.system() == "Windows": - if pathlib.Path(vs_pwsh_path).exists(): - compiler = "cl" - elif shutil.which("clang++") is not None: - compiler = "clang++" - elif shutil.which("g++") is not None: - compiler = "g++" - # On Linux, we default to GCC if it is available, otherwise we fall back to Clang. - elif platform.system() == "Linux": - if shutil.which("g++") is not None: - compiler = "g++" - elif shutil.which("clang++") is not None: - compiler = "clang++" - # On macOS, we just check if Clang is available. - elif platform.system() == "Darwin" and shutil.which("clang++") is not None: +elif platform.system() == "Windows": + # On Windows, we default to MSVC if the Visual Studio installation path exists, otherwise we fall back to Clang, and then GCC. + if vs_path is not None and vs_path.exists(): + compiler = "cl" + elif shutil.which("clang++") is not None: compiler = "clang++" + elif shutil.which("g++") is not None: + compiler = "g++" +elif platform.system() == "Linux": + # On Linux, we default to GCC if it is available, otherwise we fall back to Clang. + if shutil.which("g++") is not None: + compiler = "g++" + elif shutil.which("clang++") is not None: + compiler = "clang++" +elif platform.system() == "Darwin" and shutil.which("clang++") is not None: + # On macOS, we just check if Clang is available. + compiler = "clang++" if compiler == "": - sys.exit("Error: No compiler found!") + print_error_and_exit("Error: No compiler found!") # If a file named `compile_cpp.yaml` exists in the current working directory, read the configuration from it. All options are added to those from the command line, except the output file/folder and path to the standard library module, which are only used if not provided in the command line. Note that all folders should be specified relative to the current working directory. defines: list[str] = args.define[:] +deps: list[str] = args.deps[:] +disable_exceptions: bool = args.disable_exceptions == "true" flags: list[str] = args.flag[:] includes: list[str] = args.include[:] -modules: dict[str, list[str]] = {name: files.split(",") for module in args.module for name, files in (module.split("="),)} +try: + modules: dict[str, list[str]] = {name: files.split(",") for module in args.module for name, files in (module.split("="),)} +except ValueError: + print_error_and_exit('Error: Module specification must be in the format "module_name=module_file,dependencies,...".') output: str | None = args.output pass_args: list[str] = args.pass_args[:] -compile_yaml: pathlib.Path = pathlib.Path.cwd() / "compile_cpp.yaml" std_module: str | None = args.std_module -if not args.ignore_yaml and compile_yaml.exists(): +compile_yaml: pathlib.Path = pathlib.Path.cwd() / "compile_cpp.yaml" +if not args.ignore_config and compile_yaml.exists(): with compile_yaml.open("r") as file: - compile_config = yaml.safe_load(file) + raw_config: dict[str, Any] | Any = yaml.safe_load(file) or {} + if not isinstance(raw_config, dict): + print_error_and_exit("Error: compile_cpp.yaml must be a dictionary of options.") + compile_config = cast("dict[str, Any]", raw_config) if "defines" in compile_config: defines.extend(compile_config["defines"]) + if "deps" in compile_config: + deps.extend(compile_config["deps"]) + if args.disable_exceptions is None and "disable_exceptions" in compile_config: + disable_exceptions = compile_config["disable_exceptions"] is True if "flags" in compile_config and compiler in compile_config["flags"]: flags.extend(compile_config["flags"][compiler]) if "includes" in compile_config: @@ -134,21 +404,75 @@ if not args.ignore_yaml and compile_yaml.exists(): if std_module is None and "std_module" in compile_config and platform.system() in compile_config["std_module"] and compiler in compile_config["std_module"][platform.system()] and len(compile_config["std_module"][platform.system()][compiler]) > 0: std_module = compile_config["std_module"][platform.system()][compiler] -# Importing the C++ Standard Library is only available in C++23 mode, and currently only officially supported by MSVC and Clang. If "disable" is specified for the standard library module, we skip it; this is used to avoid infinite recursion. -use_std_module: bool = not (std_module is None or std_module == "disable" or args.std != "c++23" or compiler not in ["cl", "clang++"]) +# Determine the name of the binary file and the build folder. +binary_path: pathlib.Path | None = None +build_folder: pathlib.Path +auto_binary: bool = False +if output is not None: + # Calculate the output path relative to the current working directory. Note that if the path is absolute, `pathlib` will automatically use the absolute path instead of a relative path. + output_path: pathlib.Path = (pathlib.Path.cwd() / output).resolve() + if output.endswith(("/", "\\")) or output_path.is_dir(): + # If the output path is a directory, we use it as the build folder, and automatically determine the name of the binary file. + build_folder = output_path.resolve() + auto_binary = True + elif output_path.is_absolute(): + # If the output path is an absolute path to a file, the build folder is the file's folder. + build_folder = output_path.parent.resolve() + binary_path = output_path + else: + # If the output path is just a file name, the build folder is the current working directory by default. + build_folder = pathlib.Path.cwd() + binary_path = build_folder / output_path +else: + # If there is no output path at all, the build folder is the current working directory by default, and we automatically determine the name of the binary file. + build_folder = pathlib.Path.cwd() + auto_binary = True + +# Clear and recreate the build folder if requested. +if args.clear_output: + if build_folder == pathlib.Path.cwd(): + print_error_and_exit("Error: Cannot clear the output path if it is the current working directory.") + if build_folder.exists(): + print_if_verbose(f"Clearing output folder: {relative_or_full_path(build_folder)}", ANSI_INFO) + shutil.rmtree(build_folder) + pathlib.Path(build_folder).mkdir(exist_ok=True, parents=True) + print_if_verbose("Cleared successfully!", ANSI_SUCCESS) + else: + print_if_verbose(f"Creating empty output folder: {relative_or_full_path(build_folder)}", ANSI_INFO) + pathlib.Path(build_folder).mkdir(exist_ok=True, parents=True) + print_if_verbose("Created successfully!", ANSI_SUCCESS) + if len(source_paths) == 0: + # If no source files are specified, just clear and exit. + sys.exit(0) +else: + # Otherwise, just create the build folder if it does not exist. + pathlib.Path(build_folder).mkdir(exist_ok=True, parents=True) + +# Add the appropriate flags to disable exceptions if requested. +if disable_exceptions: + if compiler == "cl": + flags.extend(["/EHs-c-", "/D_HAS_EXCEPTIONS=0"]) + else: + flags.append("-fno-exceptions") +elif compiler == "cl": + flags.append("/EHsc") + +# Importing the C++ Standard Library is only available in C++23 mode. If "disable" is specified for the standard library module, we skip it; this is used to avoid infinite recursion. +use_std_module: bool = not (std_module is None or std_module == "disable" or args.std != "c++23") if use_std_module and std_module is not None: modules = {"std": [std_module], **modules} -# Figure out the path to the MSVC or Clang std module, if relevant. +# Figure out the path to the std module, if relevant. +gcc_std_path: str = "bits/std.cc" if use_std_module and "std" in modules and modules["std"][0].strip() == "auto": success: bool = False - if platform.system() == "Windows" and compiler == "cl": - vc_version_path: pathlib.Path = pathlib.Path(r"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt") + if platform.system() == "Windows" and compiler == "cl" and vs_path is not None: + vc_version_path: pathlib.Path = vs_path / "VC" / "Auxiliary" / "Build" / "Microsoft.VCToolsVersion.default.txt" if vc_version_path.exists(): with vc_version_path.open("r", encoding="utf-8") as vc_version_file: vc_version: str = vc_version_file.read().strip() - vc_tools_path: str = rf"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\{vc_version}" + vc_tools_path: pathlib.Path = vs_path / "VC" / "Tools" / "MSVC" / vc_version modules["std"][0] = rf"{vc_tools_path}\modules\std.ixx" success = True elif compiler == "clang++": @@ -162,248 +486,195 @@ if use_std_module and "std" in modules and modules["std"][0].strip() == "auto": modules["std"][0] = str(std_path.absolute()) success = True elif platform.system() == "Darwin": - # On macOS, the `std` module should be at `/usr/local/Cellar/llvm//share/libc++/v1/std.cppm`. We use `find` to locate it without needing to know the exact LLVM version. - llvm_path: str = subprocess.check_output(["find", "/usr/local/Cellar/llvm", "-name", "std.cppm"], text=True).strip() - if llvm_path: - modules["std"][0] = llvm_path + # On macOS, the `std` module should be at `/usr/local/Cellar/llvm//share/libc++/v1/std.cppm`. + llvm_std_path: str | None = get_llvm_std_module("/usr/local/Cellar/llvm") + if llvm_std_path: + modules["std"][0] = llvm_std_path success = True elif platform.system() == "Linux": - # On Linux, the `std` module should be at `/usr/lib/llvm-/share/libc++/v1/std.cppm`. We use `find` to locate it without needing to know the exact LLVM version. - llvm_path: str = subprocess.check_output(["find", "/usr/lib", "-name", "std.cppm"], text=True).strip() - if llvm_path: - modules["std"][0] = llvm_path + # On Linux, the `std` module should be at `/usr/lib/llvm-/share/libc++/v1/std.cppm`. + llvm_std_path: str | None = get_llvm_std_module("/usr/lib/llvm-*") + if llvm_std_path: + modules["std"][0] = llvm_std_path success = True + else: # compiler == "g++" + # In GCC the module file is always at `bits/std.cc`. + modules["std"][0] = gcc_std_path + success = True if not success: - sys.exit('Error: "auto" specified for the standard library module path, but the script could not locate it. Please specify the path manually.') + print_error_and_exit('Error: "auto" specified for the standard library module path, but the script could not locate it. Please specify the path manually.') -# Determine the name of the executable file and the build folder. -executable_path: pathlib.Path | None = None -build_folder: pathlib.Path -auto_executable: bool = False -if output is not None: - # Calculate the output path relative to the current working directory. Note that if the path is absolute, `pathlib` will automatically use the absolute path instead of a relative path. - output_path: pathlib.Path = (pathlib.Path.cwd() / output).resolve() - if output.endswith(("/", "\\")) or output_path.is_dir(): - # If the output path is a directory, we use it as the build folder, and automatically determine the name of the executable file. - build_folder = output_path.resolve() - auto_executable = True - elif output_path.is_absolute(): - # If the output path is an absolute path to a file, the build folder is the file's folder. - build_folder = output_path.parent.resolve() - executable_path = output_path - else: - # If the output path is just a file name, the build folder is the current working directory by default. - build_folder = pathlib.Path.cwd() - executable_path = build_folder / output_path -else: - # If there is no output path at all, the build folder is the current working directory by default, and we automatically determine the name of the executable file. - build_folder = pathlib.Path.cwd() - auto_executable = True +# Determine the appropriate extension for modules. +module_extension: str = "" +if compiler == "cl": + module_extension = ".ifc" +elif compiler == "clang++": + module_extension = ".pcm" +else: # compiler == "g++" + # For GCC, the extension is `.gcm`, but we do not have control over that; however, we also create an object file with the `.o` extension so we can check if the module is up to date (see below). + module_extension = ".o" -# If the user did not provide an output file name, we use the name of the first source file, appending the compiler, mode, and C++ standard. +# If we are compiling as a module, add the appropriate flags. +if args.as_module: + if compiler == "cl": + flags.extend(["/interface", "/TP", "/c"]) + elif compiler == "clang++": + flags.extend(["--precompile", "-Wno-include-angled-in-module-purview", "-Wno-reserved-module-identifier", "-xc++-module"]) + else: # compiler == "g++" + # Note: Creating an object file can be disabled with `-fmodule-only`, and it doesn't seem like the object file is actually needed, only the `.gcm` file. However, we create the object file anyway so we can check if the module is up to date, since there appears to be no way to control the name of the `.gcm` file. + flags.extend(["-fmodules", "-c", f"-fmodule-mapper=|@g++-mapper-server -r{build_folder}", "-xc++"]) + +# If the user did not provide an output file name, we use the name of the first source file, appending the compiler, mode, and C++ standard, as well as the appropriate extension. short_compiler: str = "clang" if compiler == "clang++" else "gcc" if compiler == "g++" else "msvc" suffix: str = f"{args.type}-{short_compiler}-cpp{args.std[-2:]}" -if auto_executable: - extension: str = ".exe" if platform.system() == "Windows" else "" - executable: str = f"{source_paths[0].stem}_{suffix}{extension}" - executable_path = build_folder / executable -if executable_path is None: - sys.exit("Error: Could not determine executable file!") +if auto_binary: + extension: str = module_extension if args.as_module else ".exe" if platform.system() == "Windows" else "" + module_indicator: str = "module_" if args.as_module else "" + binary_name: str = f"{source_paths[0].stem}_{module_indicator}{suffix}{extension}" + binary_path = build_folder / binary_name +if binary_path is None: + print_error_and_exit("Error: Could not determine binary file name!") -# Determine if the build folder exists, and create it if not. -if not pathlib.Path(build_folder).exists(): - pathlib.Path(build_folder).mkdir() - -# If modules are specified, pre-compile them by calling this script recursively, unless the current file is itself a module. As a special case, if the std module is used, we add it even if the current file is a module, in case the module itself wants to import the std module. -module_paths: dict[str, list[pathlib.Path]] = {} +# If modules are specified, pre-compile them by calling this script recursively. If using the `std` module, pre-compile it first. If this is a module itself, don't compile any other modules to avoid infinite recursion. (Note: This assumes that modules do not depend on each other.) +recompiled_modules: bool = False +module_paths: dict[str, list[pathlib.Path]] = {name: [(pathlib.Path.cwd() / file).resolve() for file in files] for name, files in modules.items()} +module_output_paths: dict[str, pathlib.Path] = {name: build_folder / f"{path[0].stem}_module_{suffix}{module_extension}" for name, path in module_paths.items()} +if use_std_module and "std" in modules: + flags.extend(get_module_flags("std", module_output_paths["std"])) if not args.as_module: - module_paths = {name: [(pathlib.Path.cwd() / file).resolve() for file in files] for name, files in modules.items()} -elif use_std_module and "std" in modules: - module_paths = {"std": [(pathlib.Path.cwd() / modules["std"][0]).resolve()]} -if len(modules) > 0 and (args.std in ["c++20", "c++23"]): - for name, paths in module_paths.items(): - module_flags: list[str] = args.flag[:] - module_extension: str - if compiler == "cl": - module_extension = ".ifc" - module_flags = ["/interface", "/TP", "/c"] - elif compiler == "clang++": - module_extension = ".pcm" - module_flags = ["--precompile", "-Wno-include-angled-in-module-purview", "-Wno-reserved-module-identifier", "-xc++-module"] - else: # compiler == "g++" - # Note: Creating an object file can be disabled with `-fmodule-only`, and it doesn't seem like the object file is actually needed, only the `.gcm` file. However, we create the object file anyway so we can check if the module is up to date, since there appears to be no way to control the name of the `.gcm` file. - module_extension = ".o" - module_flags = ["-fmodules-ts", "-xc++", "-c", f"-fmodule-mapper=|@g++-mapper-server -r{build_folder}"] - # Only the first file on the list needs to be compiled; the rest are dependencies that are only used to check if the compiled module is up to date. - module_file: pathlib.Path = paths[0] - module_output_path: pathlib.Path = build_folder / f"{module_file.stem}_module_{suffix}{module_extension}" - if compiler == "cl": - flags.extend(["/reference", f"{name}={module_output_path.resolve()}"]) - if name != "std" or not args.as_module: - flags.append(str(build_folder / f"{module_output_path.stem}.obj")) - elif compiler == "clang++": - flags.append(f"-fmodule-file={name}={module_output_path.resolve()}") - else: # compiler == "g++" - flags.append("-fmodules-ts") - flags.append(f"-fmodule-mapper=|@g++-mapper-server -r{build_folder}") - if module_output_path.exists(): - module_output_mod: float = module_output_path.stat().st_mtime - if not any((path.exists() and path.stat().st_mtime > module_output_mod) for path in paths): - print_if_verbose(f'Module "{name}" is up to date, skipping compilation.') - continue - try: - command: list[str] = [ - "python" if platform.system() == "Windows" else "python3", - str(pathlib.Path(__file__).resolve()), - str(module_file), - f"-o={module_output_path.resolve()}", - f"-a={args.arch}", - f"-c={compiler}", - f"-s={args.std}", - f"-t={args.type}", - # Note: Not adding the extra options from the configuration file, since they will be added by the script anyway. - *[f"-d={define}" for define in args.define], - *[f"-i={include}" for include in args.include], - *[f"-f={flag}" for flag in (args.flag + module_flags)], - # If compiling the `std` module itself, we need to pass `-u disable` to avoid infinite recursion. Otherwise, we pass along the specified module path if it exists. - *(["-u=disable"] if name == "std" else [f"-u={std_module}"] if std_module is not None else []), - "-l", - *(["-v"] if args.verbose else []), - ] - print_if_verbose(separator) - print_if_verbose(f'Compiling module "{name}" with command: {subprocess.list2cmdline(command)}') - compile_result: subprocess.CompletedProcess[str] = subprocess.run( - args=command, - check=False, - text=True, - ) - if compile_result.returncode != 0: - sys.exit(f"Module compilation failed with return code: {compile_result.returncode}.") - except Exception as exc: - sys.exit(f"Could not compile module due to exception: {exc}.") - print_if_verbose("Compiling program...") - -# Collect the full paths to the include folders, relative to the current working directory. -include_paths: list[pathlib.Path] = [(pathlib.Path.cwd() / folder).resolve() for folder in includes] - -# On macOS, make sure we are using Homebrew Clang, if available, instead of Apple Clang, which does not support C++20 modules. -compiler_path: str | None -if compiler == "clang++" and platform.system() == "Darwin": - compiler_path = "/usr/local/opt/llvm/bin/clang++" - if not pathlib.Path(compiler_path).exists(): - compiler_path = shutil.which(compiler) -else: - compiler_path = shutil.which(compiler) - -# Determine the command to execute based on the chosen compiler and parameters. -command: list[str] -if compiler == "clang++": - command = [ - compiler_path if compiler_path is not None else compiler, - *flags, - *[str(path.resolve()) for path in source_paths], - f"-std={args.std}", - "-g3" if args.type == "debug" else "-O3", - "-o", - str(executable_path), - *([] if platform.system() == "Windows" else ["-pthread"]), - *[item for include in [["-I", str(path.resolve())] for path in include_paths] for item in include], - *([f"-D{define}" for define in defines]), - ] -elif compiler == "g++": + if use_std_module and "std" in modules: + recompiled_modules |= compile_module("std", [(pathlib.Path.cwd() / modules["std"][0]).resolve()], module_output_paths["std"]) if len(modules) > 0 and (args.std in ["c++20", "c++23"]): - print_if_verbose( - "NOTE: GCC v14.2.0 appears to have an internal compiler error when compiling programs containing modules with any optimization flags other than -Og enabled. Until this is fixed, if you wish to use compiler optimizations, please either include the library as a header file or use a different compiler.", - ) - command = [ - compiler_path if compiler_path is not None else compiler, - *flags, - *[str(path.resolve()) for path in source_paths], - f"-std={args.std}", - "-ggdb3" if args.type == "debug" else "-O3" if not (len(modules) > 0 and (args.std in ["c++20", "c++23"])) else "-Og", - "-o", - str(executable_path), - *([] if platform.system() == "Windows" else ["-pthread"]), - *[item for include in [["-I", str(path.resolve())] for path in include_paths] for item in include], - *([f"-D{define}" for define in defines]), - ] -else: # compiler == "cl" - command = [ - compiler_path if compiler_path is not None else compiler, - *flags, - *[str(path.resolve()) for path in source_paths], - f"/std:{"c++latest" if args.std == "c++23" else args.std}", - *(["/Zi", f"/Fd:{executable_path.with_suffix(".pdb")}"] if args.type == "debug" else ["/O2"]), - *([f"/Fe:{executable_path}"] if not args.as_module else ["/ifcOutput", str(executable_path)]), - f"/Fo:{executable_path.with_suffix(".obj")}", - *[item for include in [["/I", str(path.resolve())] for path in include_paths] for item in include], - "/permissive-", - "/EHsc", - "/nologo", - "/Zc:__cplusplus", - *([f"/D{define}" for define in defines]), - ] + for n, p in module_paths.items(): + if n == "std": + continue + flags.extend(get_module_flags(n, module_output_paths[n])) + recompiled_modules |= compile_module(n, p, module_output_paths[n]) + print_if_verbose("Compiling program...", ANSI_INFO) -# For MSVC we also need to invoke the Visual Studio Developer PowerShell script. -if compiler == "cl": - command = ["pwsh.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", f"& '{vs_pwsh_path}' -Arch {args.arch} -HostArch {args.arch} | Out-Null; {subprocess.list2cmdline(command)}"] +# If the output binary already exists, check if we need to recompile based on whether any of the source files or their dependencies have changed. If we recompiled any modules, we need to recompile anyway, so we don't need to check the dependencies. The `force` flag overrides this check. +need_recompile: bool = True +deps_paths: list[pathlib.Path] = [(pathlib.Path.cwd() / file).resolve() for file in deps] +if not args.force and not recompiled_modules and binary_path.exists(): + binary_mod: float = binary_path.stat().st_mtime + if not any((path.exists() and path.stat().st_mtime > binary_mod) for path in (source_paths + deps_paths)): + print_if_verbose("Binary is up to date, skipping compilation.", ANSI_INFO) + need_recompile = False -print_if_verbose(separator) -print_if_verbose(f"Compiler: {compiler}") -print_if_verbose(f"C++ Standard: {args.std.upper()}") -print_if_verbose(f"Type: {args.type.title()}") -print_if_verbose(f"Source file(s): [{", ".join([str(path) for path in source_paths])}]") -print_if_verbose(f"Defines: [{", ".join(defines)}]") -print_if_verbose(f"Flags: [{", ".join(flags)}]") -print_if_verbose(f"Includes: [{(", ".join([str(path) for path in include_paths]))}]") -if args.std in ["c++20", "c++23"]: - print_if_verbose(f"Modules: [{", ".join(f"{name}={path[0]}" for name, path in module_paths.items()) if not args.as_module else ""}]") -else: - print_if_verbose("Modules: ") -print_if_verbose(f"Build folder: {build_folder}") -print_if_verbose(f"Binary file: {executable_path.name}") -print_if_verbose(f"Command: {subprocess.list2cmdline(command)}") - -# Perform the actual compilation. -print_if_verbose(separator) -print_if_verbose("Compiling...") -success = False -try: - compile_start: float = time.perf_counter() - compile_result: subprocess.CompletedProcess[str] = subprocess.run( - args=command, - check=False, - text=True, - ) - if compile_result.returncode == 0: - print_if_verbose(f"Compilation completed successfully in {time.perf_counter() - compile_start:.2f} seconds.") - success = True +if need_recompile: + include_paths: list[pathlib.Path] = [(pathlib.Path.cwd() / folder).resolve() for folder in includes] + # On macOS, make sure we are using Homebrew Clang, if available, instead of Apple Clang, which does not support C++20 modules. + compiler_path: str | None + if compiler == "clang++" and platform.system() == "Darwin": + compiler_path = "/usr/local/opt/llvm/bin/clang++" + if not pathlib.Path(compiler_path).exists(): + compiler_path = shutil.which(compiler) else: - sys.exit(f"Compilation failed with return code: {compile_result.returncode}.") -except Exception as exc: - sys.exit(f"Could not compile due to exception: {exc}.") + compiler_path = shutil.which(compiler) -# If there were compilation errors, delete the executable file if it exists, so the IDE won't mistakenly run the old version anyway. -if not success and executable_path.exists(): - executable_path.unlink() + # Determine the command to execute based on the chosen compiler and parameters. + command: list[str] + if compiler == "clang++": + command = [ + compiler_path if compiler_path is not None else compiler, + *flags, + *[str(path.resolve()) for path in source_paths], + f"-std={args.std}", + "-g3" if args.type == "debug" else "-O3", + "-o", + str(binary_path), + *[item for include in [["-I", str(path.resolve())] for path in include_paths] for item in include], + *([f"-D{define}" for define in defines]), + ] + elif compiler == "g++": + command = [ + compiler_path if compiler_path is not None else compiler, + *flags, + # In the special case of GCC and the `std` module, `bits/std.cc` must be entered without the full path. + *[(str(path.resolve()) if path != pathlib.Path(gcc_std_path) else gcc_std_path) for path in source_paths], + f"-std={args.std}", + "-ggdb3" if args.type == "debug" else "-O3", + "-o", + str(binary_path), + *[item for include in [["-I", str(path.resolve())] for path in include_paths] for item in include], + *([f"-D{define}" for define in defines]), + ] + else: # compiler == "cl" + command = [ + compiler_path if compiler_path is not None else compiler, + *flags, + *[str(path.resolve()) for path in source_paths], + f"/std:{'c++latest' if args.std == 'c++23' else args.std}", + *(["/Zi", f"/Fd:{binary_path.with_suffix('.pdb')}"] if args.type == "debug" else ["/O2"]), + *([f"/Fe:{binary_path}"] if not args.as_module else ["/ifcOutput", str(binary_path)]), + # If compiling multiple source files, we cannot specify the names of each object file, only the output folder. + f"/Fo:{binary_path.with_suffix('.obj')}" if len(source_paths) == 1 else f"/Fo:{build_folder}\\", + *[item for include in [["/I", str(path.resolve())] for path in include_paths] for item in include], + "/permissive-", + "/nologo", + "/Zc:__cplusplus", + *([f"/D{define}" for define in defines]), + ] + + # For MSVC we also need to invoke the Visual Studio Developer PowerShell script. + if compiler == "cl" and vs_path is not None: + vs_pwsh_path: pathlib.Path = vs_path / "Common7" / "Tools" / "Launch-VsDevShell.ps1" + command = ["pwsh.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", f"& '{vs_pwsh_path}' -Arch {args.arch} -HostArch {args.arch} | Out-Null; {subprocess.list2cmdline(command)}"] + + print_separator() + max_length = 16 + print_key_values("Compiler", [compiler], max_length) + print_key_values("C++ Standard", [args.std.upper()], max_length) + print_key_values("Type", [args.type.title()], max_length) + print_key_values("Source file(s)", [str(relative_or_full_path(path)) for path in source_paths], max_length) + print_key_values("Defines", defines, max_length) + print_key_values("Dependencies", [str(relative_or_full_path(path)) for path in deps_paths], max_length) + print_key_values("Flags", flags, max_length) + print_key_values("Includes", ([str(relative_or_full_path(path)) for path in include_paths]), max_length) + if args.std in ["c++20", "c++23"]: + print_key_values("Modules", [f"{name}={relative_or_full_path(path[0])}" for name, path in module_paths.items()] if not args.as_module else [""], max_length) + else: + print_key_values("Modules", [""], max_length) + print_key_values("Build folder", [str(relative_or_full_path(build_folder))], max_length) + print_key_values("Binary file", [str(relative_or_full_path(binary_path))], max_length) + print_key_values("Command", [subprocess.list2cmdline(command)], max_length) + print_separator() + + # Perform the actual compilation. + print_if_verbose("Compiling...", ANSI_INFO) + try: + _ = sys.stdout.flush() + compile_start: float = time.perf_counter() + compile_result = subprocess.run( + args=command, + check=False, + text=True, + ) + if compile_result.returncode == 0: + print_if_verbose(f"Compilation completed successfully in {time.perf_counter() - compile_start:.2f} seconds.", ANSI_SUCCESS) + else: + print_error_and_exit(f"Compilation failed with return code: {compile_result.returncode}.") + except Exception as exc: + print_error_and_exit(f"Could not compile due to exception: {exc}.") if args.run: # We run the program in the build folder. os.chdir(build_folder) - print_if_verbose(f"Running program{f" with arguments {" ".join(pass_args)}" if len(pass_args) > 0 else ""}...") - print_if_verbose(separator) + print_if_verbose(f"Running program{f' with arguments {" ".join(pass_args)}' if len(pass_args) > 0 else ''}...", ANSI_INFO) + print_separator() try: + _ = sys.stdout.flush() run_start: float = time.perf_counter() run_result: subprocess.CompletedProcess[str] = subprocess.run( - args=[executable_path, *pass_args], + args=[binary_path, *pass_args], check=False, text=True, ) - print_if_verbose(separator) + print_separator() if run_result.returncode == 0: - print_if_verbose(f"Program executed successfully in {time.perf_counter() - run_start:.2f} seconds.") + print_if_verbose(f"Program executed successfully in {time.perf_counter() - run_start:.2f} seconds.", ANSI_SUCCESS) else: - sys.exit(f"Program failed with return code: {run_result.returncode}.") + print_error_and_exit(f"Program failed with return code: {run_result.returncode}.") except Exception as exc: - print_if_verbose(separator) - sys.exit(f"Could not run program due to exception: {exc}.") + print_separator() + print_error_and_exit(f"Could not run program due to exception: {exc}.") diff --git a/scripts/test_all.py b/scripts/test_all.py deleted file mode 100644 index b4d5d18..0000000 --- a/scripts/test_all.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -██████ ███████ ████████ ██ ██ ██████ ███████ █████ ██████ ██████ ██████ ██████ ██ -██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -██████ ███████ ██ ███████ ██████ █████ ███████ ██ ██ ██████ ██ ██ ██ ██ ██ -██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ -██████ ███████ ██ ██ ██ ██ ██ ███████ ██ ██ ██████ ███████ ██ ██████ ██████ ███████ - -`BS::thread_pool`: a fast, lightweight, modern, and easy-to-use C++17/C++20/C++23 thread pool library -v5.0.0 (2024-12-19) -By Barak Shoshany -Copyright (c) 2024 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.1016/j.softx.2024.101687, SoftwareX 26 (2024) 101687, arXiv:2105.00613 - -This Python script is used in the library's development environment to compile and run the test program using all possible combinations of compilers and C++ standards available in the system, in order to test compatibility. -""" - -import argparse -import pathlib -import platform -import shutil -import subprocess -import sys -import time - -parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) -_ = parser.add_argument("--compile-only", action="store_true", help="only check that the program compiles successfully with all compilers, without running it") -parsed_args: argparse.Namespace = parser.parse_args() - - -def print_message(message: str) -> None: - """Print a message with indicator characters to differentiate it from messages from the compile script.""" - print(f"\n >>>>> {message} <<<<<\n") - - -# Determine which compilers are available. -vs_pwsh_path: str = r"C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\Launch-VsDevShell.ps1" -compilers: list[str] = [] -if pathlib.Path(vs_pwsh_path).exists(): - compilers.append("cl") -if shutil.which("clang++") is not None: - compilers.append("clang++") -# On macOS, g++ is by default just an alias for clang++, so we skip it. -if shutil.which("g++") is not None and platform.system() != "Darwin": - compilers.append("g++") - -# Compile using all available compilers using all relevant C++ standards. -standards: list[str] = ["c++17", "c++20", "c++23"] -workspace_path: pathlib.Path = pathlib.Path(__file__).parent.parent.resolve() -source_path: pathlib.Path = workspace_path / "tests" / "BS_thread_pool_test.cpp" -script_path: pathlib.Path = workspace_path / "scripts" / "compile_cpp.py" -try: - compile_start: float = time.perf_counter() - for compiler in compilers: - for std in standards: - print_message(f"Compiling with {compiler} using {std.upper()} standard...") - warnings_as_errors: list[str] = ["-f/WX"] if compiler == "cl" else ["-f-Werror"] - command: list[str] = [ - "python" if platform.system() == "Windows" else "python3", - str(script_path.resolve()), - str(source_path.resolve()), - "-c", - compiler, - "-s", - std, - "-t", - "release", - *warnings_as_errors, - *(["-r"] if not parsed_args.compile_only else []), - "-p", - "stdout", - "-p", - "log", - "-p", - "tests", - "-v", - ] - compile_result: subprocess.CompletedProcess[str] = subprocess.run( - args=command, - check=False, - text=True, - ) - if compile_result.returncode != 0: - print_message("Compilation failed, aborting script!") - sys.exit(compile_result.returncode) -except Exception as exc: - print_message(f"Could not compile due to exception: {exc}.") - sys.exit(1) -print_message(f"All tests completed successfully in {time.perf_counter() - compile_start:.2f} seconds.") diff --git a/tests/BS_thread_pool_test.cpp b/tests/BS_thread_pool_test.cpp index aad1daa..556204e 100644 --- a/tests/BS_thread_pool_test.cpp +++ b/tests/BS_thread_pool_test.cpp @@ -7,26 +7,27 @@ * * @file BS_thread_pool_test.cpp * @author Barak Shoshany (baraksh@gmail.com) (https://baraksh.com/) - * @version 5.0.0 - * @date 2024-12-19 - * @copyright Copyright (c) 2024 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.1016/j.softx.2024.101687, SoftwareX 26 (2024) 101687, arXiv:2105.00613 + * @version 5.1.0 + * @date 2026-01-03 + * @copyright Copyright (c) 2021-2026 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.1016/j.softx.2024.101687, SoftwareX 26 (2024) 101687, arXiv:2105.00613 * * @brief `BS::thread_pool`: a fast, lightweight, modern, and easy-to-use C++17/C++20/C++23 thread pool library. This program tests all aspects of the library, but is not needed in order to use the library. */ -// We need to include since if we're using `import std` it will not define any feature-test macros, including `__cpp_lib_modules`, which we need to check if `import std` is supported in the first place. +// We need to include since if we're using `import std` it will not define any feature-test macros. #ifdef __has_include #if __has_include() #include // NOLINT(misc-include-cleaner) #endif #endif -// If the macro `BS_THREAD_POOL_IMPORT_STD` is defined, import the C++ Standard Library as a module. Otherwise, include the relevant Standard Library header files. This is currently only officially supported by MSVC with Microsoft STL and LLVM Clang (NOT Apple Clang) with LLVM libc++. It is not supported by GCC with any standard library, or any compiler with GNU libstdc++. We also check that the feature is enabled by checking `__cpp_lib_modules`. However, MSVC defines this macro even in C++20 mode, which is not standards-compliant, so we check that we are in C++23 mode; MSVC currently reports `__cplusplus` as `202004L` for C++23 mode, so we use that value. -#if defined(BS_THREAD_POOL_IMPORT_STD) && defined(__cpp_lib_modules) && (__cplusplus >= 202004L) && (defined(_MSC_VER) || (defined(__clang__) && defined(_LIBCPP_VERSION) && !defined(__apple_build_version__))) +// If the macro `BS_THREAD_POOL_IMPORT_STD` is defined, import the C++ Standard Library as a module. Otherwise, include the relevant Standard Library header files. +#if defined(BS_THREAD_POOL_IMPORT_STD) && (__cplusplus >= 202004L) import std; constexpr bool using_import_std = true; #else #include + #include #include #include #include @@ -66,11 +67,14 @@ constexpr bool using_import_std = true; #ifdef __cpp_lib_format #include #endif + #ifdef __cpp_lib_semaphore + #include + #endif constexpr bool using_import_std = false; #endif // If the macro `BS_THREAD_POOL_TEST_IMPORT_MODULE` is defined, import the thread pool library as a module. Otherwise, include the header file. We also check that we are in C++20 or later. We can't use `__cpp_modules` to check if modules are supported, because Clang does not define it even in C++20 mode; its support for C++20 modules is only partial, but it does seem to work for this particular library. -#define BS_THREAD_POOL_TEST_VERSION 5, 0, 0 +#define BS_THREAD_POOL_TEST_VERSION 5, 1, 0 #if defined(BS_THREAD_POOL_TEST_IMPORT_MODULE) && (__cplusplus >= 202002L) import BS.thread_pool; static_assert(BS::thread_pool_module, "The flag BS::thread_pool_module is set to false, but the library was imported as a module. Aborting compilation."); @@ -85,25 +89,359 @@ static_assert(BS::thread_pool_version == BS::version(BS_THREAD_POOL_TEST_VERSION static_assert(BS::thread_pool_native_extensions, "Cannot test the native extensions, as the thread pool module was compiled without enabling them using the macro BS_THREAD_POOL_NATIVE_EXTENSIONS. Aborting compilation."); #endif -// A global synced stream which prints to the standard output and/or the log file. -BS::synced_stream sync_out; +namespace { +// A global synced stream which prints to the standard output. +BS::synced_stream sync_cout; + +// A global synced stream which prints to a log file. +BS::synced_stream sync_log; + +// A flag to enable or disable printing to the standard output. +bool use_stdout = true; + +// A flag to enable or disable printing to the log file. +bool use_log = false; + +// A flag to enable or disable colored output. +bool no_color = false; + +// ================================ +// std::counting_semaphore polyfill +// ================================ + +#ifdef __cpp_lib_semaphore +using std::binary_semaphore; +using std::counting_semaphore; +#else +/** + * @brief A polyfill for `std::counting_semaphore`, to be used if C++20 features are not available. A `counting_semaphore` is a synchronization primitive that allows more than one concurrent access to the same resource. The number of concurrent accessors is limited by the semaphore's counter, which is decremented when a thread acquires the semaphore and incremented when a thread releases the semaphore. If the counter is zero, a thread trying to acquire the semaphore will be blocked until another thread releases the semaphore. + * + * @tparam LeastMaxValue The least maximum value of the counter. (In this implementation, it is also the actual maximum value.) + */ +template ::max()> +class [[nodiscard]] counting_semaphore +{ + static_assert(LeastMaxValue >= 0, "The least maximum value for a counting semaphore must not be negative."); + +public: + /** + * @brief Construct a new counting semaphore with the given initial counter value. + * + * @param desired The initial counter value. + */ + constexpr explicit counting_semaphore(const std::ptrdiff_t desired) : counter(desired) {} + + // The copy and move constructors and assignment operators are deleted. The semaphore cannot be copied or moved. + counting_semaphore(const counting_semaphore&) = delete; + counting_semaphore(counting_semaphore&&) = delete; + counting_semaphore& operator=(const counting_semaphore&) = delete; + counting_semaphore& operator=(counting_semaphore&&) = delete; + ~counting_semaphore() = default; + + /** + * @brief Returns the internal counter's maximum possible value, which in this implementation is equal to `LeastMaxValue`. + * + * @return The internal counter's maximum possible value. + */ + [[nodiscard]] static constexpr std::ptrdiff_t max() noexcept + { + return LeastMaxValue; + } + + /** + * @brief Atomically decrements the internal counter by 1 if it is greater than 0; otherwise blocks until it is greater than 0 and can successfully decrement the internal counter. + */ + void acquire() + { + std::unique_lock lock(mutex); + cv.wait(lock, + [this] + { + return counter > 0; + }); + --counter; + } + + /** + * @brief Atomically increments the internal counter. Any thread(s) waiting for the counter to be greater than 0, such as due to being blocked in `acquire()`, will subsequently be unblocked. + * + * @param update The amount to increment the internal counter by. Defaults to 1. + */ + void release(const std::ptrdiff_t update = 1) + { + { + const std::scoped_lock lock(mutex); + counter += update; + } + cv.notify_all(); + } + + /** + * @brief Tries to atomically decrement the internal counter by 1 if it is greater than 0; no blocking occurs regardless. + * + * @return `true` if decremented the internal counter, `false` otherwise. + */ + bool try_acquire() + { + std::scoped_lock lock(mutex); + if (counter > 0) + { + --counter; + return true; + } + return false; + } + + /** + * @brief Tries to atomically decrement the internal counter by 1 if it is greater than 0; otherwise blocks until it is greater than 0 and can successfully decrement the internal counter, or the `rel_time` duration has been exceeded. + * + * @tparam Rep An arithmetic type representing the number of ticks to wait. + * @tparam Period An `std::ratio` representing the length of each tick in seconds. + * @param rel_time The duration the function must wait. Note that the function may wait for longer. + * @return `true` if decremented the internal counter, `false` otherwise. + */ + template + bool try_acquire_for(const std::chrono::duration& rel_time) + { + std::unique_lock lock(mutex); + if (!cv.wait_for(lock, rel_time, + [this] + { + return counter > 0; + })) + return false; + --counter; + return true; + } + + /** + * @brief Tries to atomically decrement the internal counter by 1 if it is greater than 0; otherwise blocks until it is greater than 0 and can successfully decrement the internal counter, or the `abs_time` time point has been passed. + * + * @tparam Clock The type of the clock used to measure time. + * @tparam Duration An `std::chrono::duration` type used to indicate the time point. + * @param abs_time The earliest time the function must wait until. Note that the function may wait for longer. + * @return `true` if decremented the internal counter, `false` otherwise. + */ + template + bool try_acquire_until(const std::chrono::time_point& abs_time) + { + std::unique_lock lock(mutex); + if (!cv.wait_until(lock, abs_time, + [this] + { + return counter > 0; + })) + return false; + --counter; + return true; + } + +private: + /** + * @brief A mutex used to synchronize access to the counter. + */ + mutable std::mutex mutex; + + /** + * @brief A condition variable used to wait for the counter. + */ + std::condition_variable cv; + + /** + * @brief The semaphore's counter. + */ + std::ptrdiff_t counter; +}; + +/** + * @brief A polyfill for `std::binary_semaphore`, to be used if C++20 features are not available. + */ +using binary_semaphore = counting_semaphore<1>; +#endif // ====================== // Functions for printing // ====================== /** - * @brief Print a stylized header. + * @brief An enumeration class of ANSI escape codes for formatting terminal output. + */ +enum class ansi : std::uint8_t +{ + reset = 0, + bold = 1, + dim = 2, + italic = 3, + underline = 4, + invert = 7, + strike = 9, + double_underline = 21, + normal_intensity = 22, + not_italic = 23, + not_underlined = 24, + fg_black = 30, + fg_red = 31, + fg_green = 32, + fg_yellow = 33, + fg_blue = 34, + fg_magenta = 35, + fg_cyan = 36, + fg_white = 37, + bg_black = 40, + bg_red = 41, + bg_green = 42, + bg_yellow = 43, + bg_blue = 44, + bg_magenta = 45, + bg_cyan = 46, + bg_white = 47, + fg_bright_black = 90, + fg_bright_red = 91, + fg_bright_green = 92, + fg_bright_yellow = 93, + fg_bright_blue = 94, + fg_bright_magenta = 95, + fg_bright_cyan = 96, + fg_bright_white = 97, + bg_bright_black = 100, + bg_bright_red = 101, + bg_bright_green = 102, + bg_bright_yellow = 103, + bg_bright_blue = 104, + bg_bright_magenta = 105, + bg_bright_cyan = 106, + bg_bright_white = 107 +}; + +// Constants for ANSI styles used in this program. +constexpr std::initializer_list ansi_error = {ansi::fg_bright_red}; +constexpr std::initializer_list ansi_info = {ansi::fg_bright_blue}; +constexpr std::initializer_list ansi_separator = {ansi::fg_bright_yellow}; +constexpr std::initializer_list ansi_subtitle = {ansi::fg_bright_cyan, ansi::bold}; +constexpr std::initializer_list ansi_success = {ansi::fg_bright_green}; +constexpr std::initializer_list ansi_title = {ansi::fg_bright_white, ansi::bold}; +constexpr std::initializer_list ansi_title_italic = {ansi::fg_bright_white, ansi::bold, ansi::italic}; +constexpr std::initializer_list ansi_title_underline = {ansi::fg_bright_white, ansi::bold, ansi::underline}; + +/** + * @brief Output an ANSI escape code to a stream. + * + * @param out The output stream. + * @param code The ANSI code. + * @return The output stream. + */ +inline std::ostream& operator<<(std::ostream& out, const ansi code) +{ + if (no_color) + return out; + return out << "\033[" << static_cast(code) << 'm'; +} + +/** + * @brief Output multiple ANSI escape codes to a stream. + * + * @param out The output stream. + * @param codes The ANSI codes. + * @return The output stream. + */ +inline std::ostream& operator<<(std::ostream& out, const std::initializer_list codes) +{ + if (no_color) + return out; + out << "\033["; + bool first = true; + for (const ansi code : codes) + { + if (!first) + out << ';'; + else + first = false; + out << static_cast(code); + } + return out << 'm'; +} + +/** + * @brief Print any number of items to the standard output and/or the log file. + * + * @tparam T The types of the items. + * @param items The items to print. + */ +template +void log(const T&... items) +{ + if (use_stdout) + sync_cout.print(items...); + if (use_log) + sync_log.print(items...); +} + +/** + * @brief Print any number of items to the standard output and/or the log file, followed by a newline character. + * + * @tparam T The types of the items. + * @param items The items to print. + */ +template +void logln(T&&... items) +{ + log(std::forward(items)..., '\n'); +} + +/** + * @brief Print any number of items to the standard output and/or the log file with the specified ANSI styles. The ANSI codes are only printed to the standard output, but both streams receive the printed items. + * + * @tparam T The types of the items. + * @param codes The ANSI codes. + * @param items The items to print. + */ +template +void log_ansi(const std::initializer_list codes, const T&... items) +{ + if (use_stdout) + sync_cout.print(codes, items..., ansi::reset); + if (use_log) + sync_log.print(items...); +} + +/** + * @brief Print any number of items to the standard output and/or the log file with the specified ANSI styles, followed by a newline character. The ANSI codes are only printed to the standard output, but both streams receive the printed items. + * + * @tparam T The types of the items. + * @param codes The ANSI codes. + * @param items The items to print. + */ +template +void logln_ansi(const std::initializer_list codes, T&&... items) +{ + log_ansi(codes, std::forward(items)..., '\n'); +} + +/** + * @brief Print a stylized header with the specified ANSI styles. * * @param text The text of the header. Will appear between two lines. * @param symbol The symbol to use for the lines. Default is '='. + * @param codes The ANSI codes. Default is `ansi_separator`. */ -void print_header(const std::string_view text, const char symbol = '=') +void print_header(const std::string_view text, const char symbol = '=', const std::initializer_list codes = ansi_separator) { - sync_out.println(BS::synced_stream::flush); - sync_out.println(std::string(text.length(), symbol)); - sync_out.println(text); - sync_out.println(std::string(text.length(), symbol)); + const std::string separator(text.length(), symbol); + logln_ansi(codes, BS::synced_stream::flush, '\n', separator, '\n', text, '\n', separator); +} + +/** + * @brief Print a key followed by values with different ANSI styles. + * + * @tparam T The types of the values. + * @param key The key. + * @param values The values. + */ +template +void print_key_values(const std::string_view key, T&&... values) +{ + log_ansi(ansi_title, key); + logln_ansi(ansi_subtitle, std::forward(values)...); } // ================================= @@ -128,12 +466,12 @@ void check(const bool condition) { if (condition) { - sync_out.println("-> passed."); + logln_ansi(ansi_success, "-> passed."); ++test_results::tests_succeeded; } else { - sync_out.println("-> FAILED!"); + logln_ansi(ansi_error, "-> FAILED!"); ++test_results::tests_failed; } } @@ -141,13 +479,15 @@ void check(const bool condition) /** * @brief Check if the expected result has been obtained, report the result, and keep count of the total number of successes and failures. * - * @param condition The condition to check. + * @param expected The expected result. + * @param obtained The obtained result. */ template void check(const T1& expected, const T2& obtained) { - sync_out.print("- Expected: ", expected, ", obtained: ", obtained, ' '); - check(expected == static_cast(obtained)); + const bool passed = (expected == static_cast(obtained)); + log_ansi(passed ? ansi_success : ansi_error, "- Expected: ", expected, ", obtained: ", obtained, ' '); + check(passed); } /** @@ -295,10 +635,10 @@ std::string detect_cpp_standard() return "C++17"; #elif __cplusplus == 202002L return "C++20"; -#elif (__cplusplus == 202302L) || (__cplusplus == 202004L) // MSVC currently uses 202004L for /std:c++latest. +#elif (__cplusplus == 202302L) || (defined(_MSC_VER) && __cplusplus > 202002L) // MSVC with `/std:c++latest` only guarantees `__cplusplus` is "at least one higher" than the highest supported version. return "C++23"; #else - return make_string("Other (__cplusplus = ", __cplusplus, ")"); + return "Other"; #endif } @@ -351,87 +691,87 @@ std::string detect_os() */ void print_features() { - constexpr int width = 33; - sync_out.print(std::left); + constexpr int width = 35; + log(std::left); - sync_out.print(std::setw(width), "__cpp_concepts:"); + log_ansi(ansi_title, std::setw(width), "* __cpp_concepts:"); #ifdef __cpp_concepts - sync_out.println(__cpp_concepts); + logln_ansi(ansi_subtitle, __cpp_concepts); #else - sync_out.println("N/A"); + logln_ansi(ansi_subtitle, "N/A"); #endif - sync_out.print(std::setw(width), "__cpp_exceptions:"); + log_ansi(ansi_title, std::setw(width), "* __cpp_exceptions:"); #ifdef __cpp_exceptions - sync_out.println(__cpp_exceptions); + logln_ansi(ansi_subtitle, __cpp_exceptions); #else - sync_out.println("N/A"); + logln_ansi(ansi_subtitle, "N/A"); #endif - sync_out.print(std::setw(width), "__cpp_impl_three_way_comparison:"); + log_ansi(ansi_title, std::setw(width), "* __cpp_impl_three_way_comparison:"); #ifdef __cpp_impl_three_way_comparison - sync_out.println(__cpp_impl_three_way_comparison); + logln_ansi(ansi_subtitle, __cpp_impl_three_way_comparison); #else - sync_out.println("N/A"); + logln_ansi(ansi_subtitle, "N/A"); #endif - sync_out.print(std::setw(width), "__cpp_lib_format:"); + log_ansi(ansi_title, std::setw(width), "* __cpp_lib_format:"); #ifdef __cpp_lib_format - sync_out.println(__cpp_lib_format); + logln_ansi(ansi_subtitle, __cpp_lib_format); #else - sync_out.println("N/A"); + logln_ansi(ansi_subtitle, "N/A"); #endif - sync_out.print(std::setw(width), "__cpp_lib_int_pow2:"); + log_ansi(ansi_title, std::setw(width), "* __cpp_lib_int_pow2:"); #ifdef __cpp_lib_int_pow2 - sync_out.println(__cpp_lib_int_pow2); + logln_ansi(ansi_subtitle, __cpp_lib_int_pow2); #else - sync_out.println("N/A"); + logln_ansi(ansi_subtitle, "N/A"); #endif - sync_out.print(std::setw(width), "__cpp_lib_jthread:"); + log_ansi(ansi_title, std::setw(width), "* __cpp_lib_jthread:"); #ifdef __cpp_lib_jthread - sync_out.println(__cpp_lib_jthread); + logln_ansi(ansi_subtitle, __cpp_lib_jthread); #else - sync_out.println("N/A"); + logln_ansi(ansi_subtitle, "N/A"); #endif - sync_out.print(std::setw(width), "__cpp_lib_modules:"); + log_ansi(ansi_title, std::setw(width), "* __cpp_lib_modules:"); #ifdef __cpp_lib_modules - sync_out.println(__cpp_lib_modules); + logln_ansi(ansi_subtitle, __cpp_lib_modules); #else - sync_out.println("N/A"); + logln_ansi(ansi_subtitle, "N/A"); #endif - sync_out.print(std::setw(width), "__cpp_lib_move_only_function:"); + log_ansi(ansi_title, std::setw(width), "* __cpp_lib_move_only_function:"); #ifdef __cpp_lib_move_only_function - sync_out.println(__cpp_lib_move_only_function); + logln_ansi(ansi_subtitle, __cpp_lib_move_only_function); #else - sync_out.println("N/A"); + logln_ansi(ansi_subtitle, "N/A"); #endif - sync_out.print(std::setw(width), "__cpp_lib_semaphore:"); + log_ansi(ansi_title, std::setw(width), "* __cpp_lib_semaphore:"); #ifdef __cpp_lib_semaphore - sync_out.println(__cpp_lib_semaphore); + logln_ansi(ansi_subtitle, __cpp_lib_semaphore); #else - sync_out.println("N/A"); + logln_ansi(ansi_subtitle, "N/A"); #endif - sync_out.print(std::setw(width), "__cpp_modules:"); + log_ansi(ansi_title, std::setw(width), "* __cpp_modules:"); #ifdef __cpp_modules - sync_out.println(__cpp_modules); + logln_ansi(ansi_subtitle, __cpp_modules); #else - sync_out.println("N/A"); + logln_ansi(ansi_subtitle, "N/A"); #endif - sync_out.print(std::setw(width), "__has_include():"); + log_ansi(ansi_title, std::setw(width), "* __has_include():"); #if __has_include() - sync_out.println("true"); + logln_ansi(ansi_subtitle, "true"); #else - sync_out.println("false"); + logln_ansi(ansi_subtitle, "false"); #endif - sync_out.println(std::right); + logln(std::right); } // ========================================= @@ -448,7 +788,7 @@ std::vector obtain_unique_threads(BS::thread_pool<>& pool) const std::size_t num_tasks = pool.get_thread_count() * 2; std::vector thread_ids(num_tasks); std::atomic total_count = 0; - BS::counting_semaphore sem(0); + counting_semaphore sem(0); for (std::thread::id& tid : thread_ids) { pool.detach_task( @@ -473,12 +813,12 @@ std::vector obtain_unique_threads(BS::thread_pool<>& pool) void check_constructor() { BS::thread_pool pool; - sync_out.println("Checking that the thread pool reports a number of threads equal to the hardware concurrency..."); + logln("Checking that the thread pool reports a number of threads equal to the hardware concurrency..."); check(std::thread::hardware_concurrency(), pool.get_thread_count()); - sync_out.println("Checking that the manually counted number of unique thread IDs is equal to the reported number of threads..."); + logln("Checking that the manually counted number of unique thread IDs is equal to the reported number of threads..."); const std::vector unique_threads = obtain_unique_threads(pool); check(pool.get_thread_count(), unique_threads.size()); - sync_out.println("Checking that the unique thread IDs obtained are the same as those reported by get_thread_ids()..."); + logln("Checking that the unique thread IDs obtained are the same as those reported by get_thread_ids()..."); std::vector threads_from_pool = pool.get_thread_ids(); std::sort(threads_from_pool.begin(), threads_from_pool.end()); check(threads_from_pool == unique_threads); @@ -491,14 +831,14 @@ void check_reset() { BS::thread_pool pool; pool.reset(static_cast(std::thread::hardware_concurrency()) * 2); - sync_out.println("Checking that after reset() the thread pool reports a number of threads equal to double the hardware concurrency..."); + logln("Checking that after reset() the thread pool reports a number of threads equal to double the hardware concurrency..."); check(std::thread::hardware_concurrency() * 2, pool.get_thread_count()); - sync_out.println("Checking that after reset() the manually counted number of unique thread IDs is equal to the reported number of threads..."); + logln("Checking that after reset() the manually counted number of unique thread IDs is equal to the reported number of threads..."); check(pool.get_thread_count(), obtain_unique_threads(pool).size()); pool.reset(std::thread::hardware_concurrency()); - sync_out.println("Checking that after a second reset() the thread pool reports a number of threads equal to the hardware concurrency..."); + logln("Checking that after a second reset() the thread pool reports a number of threads equal to the hardware concurrency..."); check(std::thread::hardware_concurrency(), pool.get_thread_count()); - sync_out.println("Checking that after a second reset() the manually counted number of unique thread IDs is equal to the reported number of threads..."); + logln("Checking that after a second reset() the manually counted number of unique thread IDs is equal to the reported number of threads..."); check(pool.get_thread_count(), obtain_unique_threads(pool).size()); } @@ -545,7 +885,7 @@ private: void check_task(const std::string_view which_func) { BS::thread_pool pool; - sync_out.println("Checking that ", which_func, " works for a function with no arguments or return value..."); + logln("Checking that ", which_func, " works for a function with no arguments or return value..."); { bool flag = false; const auto func = [&flag] @@ -563,7 +903,7 @@ void check_task(const std::string_view which_func) } check(flag); } - sync_out.println("Checking that ", which_func, " works for a function with one argument and no return value..."); + logln("Checking that ", which_func, " works for a function with one argument and no return value..."); { bool flag = false; const auto func = [](bool& flag_) @@ -590,7 +930,7 @@ void check_task(const std::string_view which_func) } check(flag); } - sync_out.println("Checking that ", which_func, " works for a function with two arguments and no return value..."); + logln("Checking that ", which_func, " works for a function with two arguments and no return value..."); { bool flag1 = false; bool flag2 = false; @@ -620,7 +960,7 @@ void check_task(const std::string_view which_func) } if (which_func == "submit_task()") { - sync_out.println("Checking that submit_task() works for a function with no arguments and a return value..."); + logln("Checking that submit_task() works for a function with no arguments and a return value..."); { bool flag = false; const auto func = [&flag] @@ -630,7 +970,7 @@ void check_task(const std::string_view which_func) std::future flag_future = pool.submit_task(func); check(flag_future.get() && flag); } - sync_out.println("Checking that submit_task() works for a function with one argument and a return value..."); + logln("Checking that submit_task() works for a function with one argument and a return value..."); { bool flag = false; const auto func = [](bool& flag_) @@ -644,7 +984,7 @@ void check_task(const std::string_view which_func) }); check(flag_future.get() && flag); } - sync_out.println("Checking that submit_task() works for a function with two arguments and a return value..."); + logln("Checking that submit_task() works for a function with two arguments and a return value..."); { bool flag1 = false; bool flag2 = false; @@ -660,7 +1000,7 @@ void check_task(const std::string_view which_func) check(flag_future.get() && flag1 && flag2); } } - sync_out.println("Checking that ", which_func, " does not create unnecessary copies of the function object..."); + logln("Checking that ", which_func, " does not create unnecessary copies of the function object..."); { bool copied = false; bool moved = false; @@ -680,10 +1020,10 @@ void check_task(const std::string_view which_func) } check(!copied && moved); } - sync_out.println("Checking that ", which_func, " correctly accepts arguments passed by value, reference, and constant reference..."); + logln("Checking that ", which_func, " correctly accepts arguments passed by value, reference, and constant reference..."); { { - sync_out.println("Value:"); + logln("Value:"); const std::int64_t pass_me_by_value = 0; const auto func_value = [](std::int64_t passed_by_value) { @@ -711,7 +1051,7 @@ void check_task(const std::string_view which_func) check(pass_me_by_value == 0); } { - sync_out.println("Reference:"); + logln("Reference:"); std::int64_t pass_me_by_ref = 0; const auto func_ref = [](std::int64_t& passed_by_ref) { @@ -738,9 +1078,9 @@ void check_task(const std::string_view which_func) check(pass_me_by_ref == 1); } { - sync_out.println("Constant reference:"); + logln("Constant reference:"); std::int64_t pass_me_by_cref = 0; - BS::binary_semaphore sem(0); + binary_semaphore sem(0); const auto func_cref = [&sem](const std::int64_t& passed_by_cref) { sem.acquire(); @@ -880,7 +1220,7 @@ private: void check_member_function() { BS::thread_pool pool; - sync_out.println("Checking that detach_task() works for a member function with no arguments or return value..."); + logln("Checking that detach_task() works for a member function with no arguments or return value..."); { flag_class flag(pool); pool.detach_task( @@ -891,7 +1231,7 @@ void check_member_function() pool.wait(); check(flag.get_flag()); } - sync_out.println("Checking that detach_task() works for a member function with one argument and no return value..."); + logln("Checking that detach_task() works for a member function with one argument and no return value..."); { flag_class flag(pool); pool.detach_task( @@ -902,7 +1242,7 @@ void check_member_function() pool.wait(); check(flag.get_flag()); } - sync_out.println("Checking that submit_task() works for a member function with no arguments or return value..."); + logln("Checking that submit_task() works for a member function with no arguments or return value..."); { flag_class flag(pool); pool.submit_task( @@ -913,7 +1253,7 @@ void check_member_function() .wait(); check(flag.get_flag()); } - sync_out.println("Checking that submit_task() works for a member function with one argument and no return value..."); + logln("Checking that submit_task() works for a member function with one argument and no return value..."); { flag_class flag(pool); pool.submit_task( @@ -924,7 +1264,7 @@ void check_member_function() .wait(); check(flag.get_flag()); } - sync_out.println("Checking that submit_task() works for a member function with no arguments and a return value..."); + logln("Checking that submit_task() works for a member function with no arguments and a return value..."); { flag_class flag(pool); std::future flag_future = pool.submit_task( @@ -934,7 +1274,7 @@ void check_member_function() }); check(flag_future.get() && flag.get_flag()); } - sync_out.println("Checking that submit_task() works for a member function with one argument and a return value..."); + logln("Checking that submit_task() works for a member function with one argument and a return value..."); { flag_class flag(pool); std::future flag_future = pool.submit_task( @@ -952,32 +1292,32 @@ void check_member_function() void check_member_function_within_object() { BS::thread_pool pool; - sync_out.println("Checking that detach_task() works within an object for a member function with no arguments or return value..."); + logln("Checking that detach_task() works within an object for a member function with no arguments or return value..."); { flag_class flag(pool); flag.detach_test_flag_no_args(); } - sync_out.println("Checking that detach_task() works within an object for a member function with one argument and no return value..."); + logln("Checking that detach_task() works within an object for a member function with one argument and no return value..."); { flag_class flag(pool); flag.detach_test_flag_one_arg(); } - sync_out.println("Checking that submit_task() works within an object for a member function with no arguments or return value..."); + logln("Checking that submit_task() works within an object for a member function with no arguments or return value..."); { flag_class flag(pool); flag.submit_test_flag_no_args(); } - sync_out.println("Checking that submit_task() works within an object for a member function with one argument and no return value..."); + logln("Checking that submit_task() works within an object for a member function with one argument and no return value..."); { flag_class flag(pool); flag.submit_test_flag_one_arg(); } - sync_out.println("Checking that submit_task() works within an object for a member function with no arguments and a return value..."); + logln("Checking that submit_task() works within an object for a member function with no arguments and a return value..."); { flag_class flag(pool); flag.submit_test_flag_no_args_return(); } - sync_out.println("Checking that submit_task() works within an object for a member function with one argument and a return value..."); + logln("Checking that submit_task() works within an object for a member function with one argument and a return value..."); { flag_class flag(pool); flag.submit_test_flag_one_arg_return(); @@ -991,7 +1331,7 @@ void normal_func() check_callables_flag = true; } -struct functor +struct function_object { void operator()() { @@ -1014,23 +1354,23 @@ void check_callables() { BS::thread_pool pool; - sync_out.println("Checking normal function..."); + logln("Checking normal function..."); pool.submit_task(normal_func).wait(); check(check_callables_flag); - sync_out.println("Checking function pointer..."); + logln("Checking function pointer..."); check_callables_flag = false; - void (*const func_ptr)() = normal_func; + void (*const func_ptr)() = normal_func; // NOLINT(misc-const-correctness) pool.submit_task(func_ptr).wait(); check(check_callables_flag); - sync_out.println("Checking pointer to static member function..."); + logln("Checking pointer to static member function..."); check_callables_flag = false; auto member_func_ptr = has_member_function::member_function; pool.submit_task(member_func_ptr).wait(); check(check_callables_flag); - sync_out.println("Checking lambda expression..."); + logln("Checking lambda expression..."); check_callables_flag = false; const auto lambda = [] { @@ -1039,7 +1379,7 @@ void check_callables() pool.submit_task(lambda).wait(); check(check_callables_flag); - sync_out.println("Checking std::function..."); + logln("Checking std::function..."); check_callables_flag = false; const std::function function = [] { @@ -1049,7 +1389,7 @@ void check_callables() check(check_callables_flag); #ifdef __cpp_lib_move_only_function - sync_out.println("Checking std::move_only_function..."); + logln("Checking std::move_only_function..."); check_callables_flag = false; std::move_only_function move_only_function = [] { @@ -1058,10 +1398,10 @@ void check_callables() pool.submit_task(std::move(move_only_function)).wait(); check(check_callables_flag); #else - sync_out.println("Note: std::move_only_function not available, skipping the corresponding test."); + logln_ansi(ansi_info, "Note: std::move_only_function not available, skipping the corresponding test."); #endif - sync_out.println("Checking std::bind..."); + logln("Checking std::bind..."); check_callables_flag = false; const auto lambda_for_bind = [](std::atomic& flag) { @@ -1070,10 +1410,10 @@ void check_callables() pool.submit_task(std::bind(lambda_for_bind, std::ref(check_callables_flag))).wait(); check(check_callables_flag); - sync_out.println("Checking functor..."); + logln("Checking function object..."); check_callables_flag = false; - const functor functor_instance; - pool.submit_task(functor_instance).wait(); + const function_object func_obj_instance; + pool.submit_task(func_obj_instance).wait(); check(check_callables_flag); } @@ -1099,7 +1439,7 @@ void check_wait() flags[i] = true; }); } - sync_out.println("Waiting for tasks..."); + logln("Waiting for tasks..."); pool.wait(); check(all_flags_set(flags)); } @@ -1112,22 +1452,22 @@ void check_wait_blocks() constexpr std::chrono::milliseconds sleep_time(100); constexpr std::size_t num_waiting_tasks = 4; BS::thread_pool pool; - BS::binary_semaphore sem(0); - sync_out.println("Checking that wait() correctly blocks all external threads that call it..."); + binary_semaphore sem(0); + logln("Checking that wait() correctly blocks all external threads that call it..."); pool.detach_task( [&sem] { - sync_out.println("Task submitted to pool 1 and waiting to be released..."); + logln("Task submitted to pool 1 and waiting to be released..."); sem.acquire(); - sync_out.println("Task released."); + logln("Task released."); }); BS::thread_pool temp_pool(num_waiting_tasks); std::vector> flags(num_waiting_tasks); const auto waiting_task = [&flags, &pool](const std::size_t task_num) { - sync_out.println("Task ", task_num, " submitted to pool 2 and waiting for pool 1's task to finish..."); + logln("Task ", task_num, " submitted to pool 2 and waiting for pool 1's task to finish..."); pool.wait(); - sync_out.println("Task ", task_num, " finished waiting."); + logln("Task ", task_num, " finished waiting."); flags[task_num] = true; }; for (std::size_t i = 0; i < num_waiting_tasks; ++i) @@ -1153,7 +1493,7 @@ void check_wait_for() constexpr std::chrono::milliseconds long_sleep_time(250); constexpr std::chrono::milliseconds short_sleep_time(10); BS::thread_pool pool; - sync_out.println("Checking that wait_for() works..."); + logln("Checking that wait_for() works..."); std::atomic done = false; pool.detach_task( [&done, long_sleep_time] @@ -1161,10 +1501,10 @@ void check_wait_for() std::this_thread::sleep_for(long_sleep_time); done = true; }); - sync_out.println("Task that lasts ", long_sleep_time.count(), "ms submitted. Waiting for ", short_sleep_time.count(), "ms..."); + logln("Task that lasts ", long_sleep_time.count(), "ms submitted. Waiting for ", short_sleep_time.count(), "ms..."); pool.wait_for(short_sleep_time); check(!done); - sync_out.println("Waiting for ", long_sleep_time.count() * 2, "ms..."); + logln("Waiting for ", long_sleep_time.count() * 2, "ms..."); pool.wait_for(long_sleep_time * 2); check(done); } @@ -1177,7 +1517,7 @@ void check_wait_until() constexpr std::chrono::milliseconds long_sleep_time(250); constexpr std::chrono::milliseconds short_sleep_time(10); BS::thread_pool pool; - sync_out.println("Checking that wait_until() works..."); + logln("Checking that wait_until() works..."); std::atomic done = false; pool.detach_task( [&done, long_sleep_time] @@ -1186,10 +1526,10 @@ void check_wait_until() done = true; }); const std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); - sync_out.println("Task that lasts ", long_sleep_time.count(), "ms submitted. Waiting until ", short_sleep_time.count(), "ms from submission time..."); + logln("Task that lasts ", long_sleep_time.count(), "ms submitted. Waiting until ", short_sleep_time.count(), "ms from submission time..."); pool.wait_until(now + short_sleep_time); check(!done); - sync_out.println("Waiting until ", long_sleep_time.count() * 2, "ms from submission time..."); + logln("Waiting until ", long_sleep_time.count() * 2, "ms from submission time..."); pool.wait_until(now + long_sleep_time * 2); check(done); } @@ -1204,7 +1544,7 @@ void check_wait_multiple_deadlock() { constexpr std::chrono::milliseconds sleep_time(500); constexpr std::size_t n_waiting_tasks = 1000; - sync_out.println("Checking for deadlocks when waiting for tasks..."); + logln("Checking for deadlocks when waiting for tasks..."); BS::thread_pool pool(1); pool.detach_task( [sleep_time] @@ -1228,17 +1568,17 @@ void check_wait_multiple_deadlock() check_wait_multiple_deadlock_pool.wait_for(sleep_time * 2); if (count == n_waiting_tasks) { - sync_out.println("All waiting tasks successfully finished!"); + logln_ansi(ansi_success, "All waiting tasks successfully finished!"); passed = true; break; } if (count == old_count) { - sync_out.println("Error: deadlock detected!"); + logln_ansi(ansi_error, "Error: deadlock detected!"); passed = false; break; } - sync_out.println(count, " tasks out of ", n_waiting_tasks, " finished waiting..."); + logln(count, " tasks out of ", n_waiting_tasks, " finished waiting..."); } check(passed); } @@ -1253,7 +1593,7 @@ BS::wdc_thread_pool check_wait_self_deadlock_pool; void check_wait_self_deadlock() { constexpr std::chrono::milliseconds sleep_time(100); - sync_out.println("Checking for deadlocks when waiting from within a thread of the same pool..."); + logln("Checking for deadlocks when waiting from within a thread of the same pool..."); std::atomic passed = false; check_wait_self_deadlock_pool.detach_task( [&passed] @@ -1288,13 +1628,13 @@ void check_wait_self_deadlock() */ bool check_loop_no_return(BS::thread_pool<>& pool, const std::int64_t random_start, const std::int64_t random_end, const std::size_t num_tasks, const std::string_view which_func) { - sync_out.println("Verifying that ", which_func, " from ", random_start, " to ", random_end, " with ", num_tasks, num_tasks == 1 ? " task" : " tasks", " modifies all indices exactly once..."); + logln("Verifying that ", which_func, " from ", random_start, " to ", random_end, " with ", num_tasks, num_tasks == 1 ? " task" : " tasks", " modifies all indices exactly once..."); const std::size_t num_indices = static_cast(random_end - random_start); std::vector> flags(num_indices); std::atomic indices_out_of_range = false; const auto loop = [&flags, random_start, random_end, &indices_out_of_range](const std::int64_t idx) { - if (idx < random_start || idx > random_end) + if (idx < random_start || idx >= random_end) indices_out_of_range = true; else ++flags[static_cast(idx - random_start)]; @@ -1310,7 +1650,7 @@ bool check_loop_no_return(BS::thread_pool<>& pool, const std::int64_t random_sta } if (indices_out_of_range) { - sync_out.println("Error: Loop indices out of range!"); + logln_ansi(ansi_error, "Error: Loop indices out of range!"); return false; } return all_flags_equal(flags, 1); @@ -1334,11 +1674,11 @@ void check_loop() const std::pair indices = random_pair(-range, range); check(check_loop_no_return(pool, indices.first, indices.second, random(1, pool.get_thread_count()), "submit_loop()")); } - sync_out.println("Verifying that detach_loop() with identical start and end indices does nothing..."); + logln("Verifying that detach_loop() with identical start and end indices does nothing..."); { std::atomic count = 0; const std::int64_t index = random(-range, range); - sync_out.println("Range: ", index, " to ", index); + logln("Range: ", index, " to ", index); pool.detach_loop(index, index, [&count](const std::int64_t) { @@ -1347,11 +1687,11 @@ void check_loop() pool.wait(); check(count == 0); } - sync_out.println("Verifying that submit_loop() with identical start and end indices does nothing..."); + logln("Verifying that submit_loop() with identical start and end indices does nothing..."); { std::atomic count = 0; const std::int64_t index = random(-range, range); - sync_out.println("Range: ", index, " to ", index); + logln("Range: ", index, " to ", index); pool.submit_loop(index, index, [&count](const std::int64_t) { @@ -1360,11 +1700,11 @@ void check_loop() .wait(); check(count == 0); } - sync_out.println("Verifying that detach_loop() with end index smaller than the start index does nothing..."); + logln("Verifying that detach_loop() with end index smaller than the start index does nothing..."); { std::atomic count = 0; const std::pair indices = random_pair(-range, range); - sync_out.println("Range: ", indices.second, " to ", indices.first); + logln("Range: ", indices.second, " to ", indices.first); pool.detach_loop(indices.second, indices.first, [&count](const std::int64_t) { @@ -1373,11 +1713,11 @@ void check_loop() pool.wait(); check(count == 0); } - sync_out.println("Verifying that submit_loop() with end index smaller than the start index does nothing..."); + logln("Verifying that submit_loop() with end index smaller than the start index does nothing..."); { std::atomic count = 0; const std::pair indices = random_pair(-range, range); - sync_out.println("Range: ", indices.second, " to ", indices.first); + logln("Range: ", indices.second, " to ", indices.first); pool.submit_loop(indices.second, indices.first, [&count](const std::int64_t) { @@ -1386,12 +1726,12 @@ void check_loop() .wait(); check(count == 0); } - sync_out.println("Trying detach_loop() with a number of tasks larger than the number of indices:"); + logln("Trying detach_loop() with a number of tasks larger than the number of indices:"); { const std::int64_t start = random(-range, range); check(check_loop_no_return(pool, start, start + random(0, static_cast(pool.get_thread_count() * 2)), random(pool.get_thread_count() * 2, pool.get_thread_count() * 4), "detach_loop()")); } - sync_out.println("Trying submit_loop() with a number of tasks larger than the number of indices:"); + logln("Trying submit_loop() with a number of tasks larger than the number of indices:"); { const std::int64_t start = random(-range, range); check(check_loop_no_return(pool, start, start + random(0, static_cast(pool.get_thread_count() * 2)), random(pool.get_thread_count() * 2, pool.get_thread_count() * 4), "submit_loop()")); @@ -1410,7 +1750,7 @@ void check_loop() */ bool check_blocks_no_return(BS::thread_pool<>& pool, const std::int64_t random_start, const std::int64_t random_end, const std::size_t num_tasks, const std::string_view which_func) { - sync_out.println("Verifying that ", which_func, " from ", random_start, " to ", random_end, " with ", num_tasks, num_tasks == 1 ? " task" : " tasks", " modifies all indices exactly once..."); + logln("Verifying that ", which_func, " from ", random_start, " to ", random_end, " with ", num_tasks, num_tasks == 1 ? " task" : " tasks", " modifies all indices exactly once..."); const std::size_t num_indices = static_cast(random_end - random_start); std::vector> flags(num_indices); std::atomic indices_out_of_range = false; @@ -1437,7 +1777,7 @@ bool check_blocks_no_return(BS::thread_pool<>& pool, const std::int64_t random_s } if (indices_out_of_range) { - sync_out.println("Error: Block indices out of range!"); + logln_ansi(ansi_error, "Error: Block indices out of range!"); return false; } return all_flags_equal(flags, 1); @@ -1453,7 +1793,7 @@ bool check_blocks_no_return(BS::thread_pool<>& pool, const std::int64_t random_s */ void check_blocks_return(BS::thread_pool<>& pool, const std::int64_t random_start, const std::int64_t random_end, const std::size_t num_tasks) { - sync_out.println("Verifying that submit_blocks() from ", random_start, " to ", random_end, " with ", num_tasks, num_tasks == 1 ? " task" : " tasks", " correctly sums all indices..."); + logln("Verifying that submit_blocks() from ", random_start, " to ", random_end, " with ", num_tasks, num_tasks == 1 ? " task" : " tasks", " correctly sums all indices..."); const auto loop = [](const std::int64_t start, const std::int64_t end) { std::int64_t total = 0; @@ -1491,11 +1831,11 @@ void check_blocks() const std::pair indices = random_pair(-range, range); check_blocks_return(pool, indices.first, indices.second, random(1, pool.get_thread_count())); } - sync_out.println("Verifying that detach_blocks() with identical start and end indices does nothing..."); + logln("Verifying that detach_blocks() with identical start and end indices does nothing..."); { std::atomic count = 0; const std::int64_t index = random(-range, range); - sync_out.println("Range: ", index, " to ", index); + logln("Range: ", index, " to ", index); pool.detach_blocks(index, index, [&count](const std::int64_t, const std::int64_t) { @@ -1504,11 +1844,11 @@ void check_blocks() pool.wait(); check(count == 0); } - sync_out.println("Verifying that submit_blocks() with identical start and end indices does nothing..."); + logln("Verifying that submit_blocks() with identical start and end indices does nothing..."); { std::atomic count = 0; const std::int64_t index = random(-range, range); - sync_out.println("Range: ", index, " to ", index); + logln("Range: ", index, " to ", index); pool.submit_blocks(index, index, [&count](const std::int64_t, const std::int64_t) { @@ -1517,11 +1857,11 @@ void check_blocks() .wait(); check(count == 0); } - sync_out.println("Verifying that detach_blocks() with end index smaller than the start index does nothing..."); + logln("Verifying that detach_blocks() with end index smaller than the start index does nothing..."); { std::atomic count = 0; const std::pair indices = random_pair(-range, range); - sync_out.println("Range: ", indices.second, " to ", indices.first); + logln("Range: ", indices.second, " to ", indices.first); pool.detach_blocks(indices.second, indices.first, [&count](const std::int64_t, const std::int64_t) { @@ -1530,11 +1870,11 @@ void check_blocks() pool.wait(); check(count == 0); } - sync_out.println("Verifying that submit_blocks() with end index smaller than the start index does nothing..."); + logln("Verifying that submit_blocks() with end index smaller than the start index does nothing..."); { std::atomic count = 0; const std::pair indices = random_pair(-range, range); - sync_out.println("Range: ", indices.second, " to ", indices.first); + logln("Range: ", indices.second, " to ", indices.first); pool.submit_blocks(indices.second, indices.first, [&count](const std::int64_t, const std::int64_t) { @@ -1543,12 +1883,12 @@ void check_blocks() .wait(); check(count == 0); } - sync_out.println("Trying detach_blocks() with a number of tasks larger than the number of indices:"); + logln("Trying detach_blocks() with a number of tasks larger than the number of indices:"); { const std::int64_t start = random(-range, range); check(check_blocks_no_return(pool, start, start + random(0, static_cast(pool.get_thread_count() * 2)), random(pool.get_thread_count() * 2, pool.get_thread_count() * 4), "detach_blocks()")); } - sync_out.println("Trying submit_blocks() with a number of tasks larger than the number of indices:"); + logln("Trying submit_blocks() with a number of tasks larger than the number of indices:"); { const std::int64_t start = random(-range, range); check(check_blocks_no_return(pool, start, start + random(0, static_cast(pool.get_thread_count() * 2)), random(pool.get_thread_count() * 2, pool.get_thread_count() * 4), "submit_blocks()")); @@ -1570,13 +1910,13 @@ void check_blocks() */ bool check_sequence_no_return(BS::thread_pool<>& pool, const std::int64_t random_start, const std::int64_t random_end, const std::string_view which_func) { - sync_out.println("Verifying that ", which_func, " from ", random_start, " to ", random_end, " modifies all indices exactly once..."); + logln("Verifying that ", which_func, " from ", random_start, " to ", random_end, " modifies all indices exactly once..."); const std::size_t num_indices = static_cast(random_end - random_start); std::vector> flags(num_indices); std::atomic indices_out_of_range = false; const auto sequence = [&flags, random_start, random_end, &indices_out_of_range](const std::int64_t index) { - if (index < random_start || index > random_end) + if (index < random_start || index >= random_end) indices_out_of_range = true; else ++flags[static_cast(index - random_start)]; @@ -1592,7 +1932,7 @@ bool check_sequence_no_return(BS::thread_pool<>& pool, const std::int64_t random } if (indices_out_of_range) { - sync_out.println("Error: Sequence indices out of range!"); + logln_ansi(ansi_error, "Error: Sequence indices out of range!"); return false; } return all_flags_equal(flags, 1); @@ -1607,7 +1947,7 @@ bool check_sequence_no_return(BS::thread_pool<>& pool, const std::int64_t random */ void check_sequence_return(BS::thread_pool<>& pool, const std::int64_t random_start, const std::int64_t random_end) { - sync_out.println("Verifying that submit_sequence() from ", random_start, " to ", random_end, " correctly sums all squares of indices..."); + logln("Verifying that submit_sequence() from ", random_start, " to ", random_end, " correctly sums all squares of indices..."); const auto sequence = [](const std::int64_t index) { return index * index; @@ -1645,11 +1985,11 @@ void check_sequence() const std::pair indices = random_pair(-range, range); check_sequence_return(pool, indices.first, indices.second); } - sync_out.println("Verifying that detach_sequence() with identical start and end indices does nothing..."); + logln("Verifying that detach_sequence() with identical start and end indices does nothing..."); { std::atomic count = 0; const std::int64_t index = random(-range, range); - sync_out.println("Range: ", index, " to ", index); + logln("Range: ", index, " to ", index); pool.detach_sequence(index, index, [&count](const std::int64_t) { @@ -1658,11 +1998,11 @@ void check_sequence() pool.wait(); check(count == 0); } - sync_out.println("Verifying that submit_sequence() with identical start and end indices does nothing..."); + logln("Verifying that submit_sequence() with identical start and end indices does nothing..."); { std::atomic count = 0; const std::int64_t index = random(-range, range); - sync_out.println("Range: ", index, " to ", index); + logln("Range: ", index, " to ", index); pool.submit_sequence(index, index, [&count](const std::int64_t) { @@ -1671,11 +2011,11 @@ void check_sequence() .wait(); check(count == 0); } - sync_out.println("Verifying that detach_sequence() with end index smaller than the start index does nothing..."); + logln("Verifying that detach_sequence() with end index smaller than the start index does nothing..."); { std::atomic count = 0; const std::pair indices = random_pair(-range, range); - sync_out.println("Range: ", indices.second, " to ", indices.first); + logln("Range: ", indices.second, " to ", indices.first); pool.detach_sequence(indices.second, indices.first, [&count](const std::int64_t) { @@ -1684,11 +2024,11 @@ void check_sequence() pool.wait(); check(count == 0); } - sync_out.println("Verifying that submit_sequence() with end index smaller than the start index does nothing..."); + logln("Verifying that submit_sequence() with end index smaller than the start index does nothing..."); { std::atomic count = 0; const std::pair indices = random_pair(-range, range); - sync_out.println("Range: ", indices.second, " to ", indices.first); + logln("Range: ", indices.second, " to ", indices.first); pool.submit_sequence(indices.second, indices.first, [&count](const std::int64_t) { @@ -1710,37 +2050,37 @@ void check_task_monitoring() { constexpr std::chrono::milliseconds sleep_time(300); const std::size_t num_threads = std::min(std::thread::hardware_concurrency(), 4); - sync_out.println("Creating pool with ", num_threads, " threads."); + logln("Creating pool with ", num_threads, " threads."); BS::thread_pool pool(num_threads); - sync_out.println("Submitting ", num_threads * 3, " tasks."); - BS::counting_semaphore sem(0); + logln("Submitting ", num_threads * 3, " tasks."); + counting_semaphore sem(0); for (std::size_t i = 0; i < num_threads * 3; ++i) { pool.detach_task( [i, &sem] { sem.acquire(); - sync_out.println("Task ", i, " released."); + logln("Task ", i, " released."); }); } std::this_thread::sleep_for(sleep_time); - sync_out.println("After submission, should have: ", num_threads * 3, " tasks total, ", num_threads, " tasks running, ", num_threads * 2, " tasks queued..."); - sync_out.print("Result: ", pool.get_tasks_total(), " tasks total, ", pool.get_tasks_running(), " tasks running, ", pool.get_tasks_queued(), " tasks queued "); + logln("After submission, should have: ", num_threads * 3, " tasks total, ", num_threads, " tasks running, ", num_threads * 2, " tasks queued..."); + log("Result: ", pool.get_tasks_total(), " tasks total, ", pool.get_tasks_running(), " tasks running, ", pool.get_tasks_queued(), " tasks queued "); check(pool.get_tasks_total() == num_threads * 3 && pool.get_tasks_running() == num_threads && pool.get_tasks_queued() == num_threads * 2); sem.release(static_cast(num_threads)); std::this_thread::sleep_for(sleep_time); - sync_out.println("After releasing ", num_threads, " tasks, should have: ", num_threads * 2, " tasks total, ", num_threads, " tasks running, ", num_threads, " tasks queued..."); - sync_out.print("Result: ", pool.get_tasks_total(), " tasks total, ", pool.get_tasks_running(), " tasks running, ", pool.get_tasks_queued(), " tasks queued "); + logln("After releasing ", num_threads, " tasks, should have: ", num_threads * 2, " tasks total, ", num_threads, " tasks running, ", num_threads, " tasks queued..."); + log("Result: ", pool.get_tasks_total(), " tasks total, ", pool.get_tasks_running(), " tasks running, ", pool.get_tasks_queued(), " tasks queued "); check(pool.get_tasks_total() == num_threads * 2 && pool.get_tasks_running() == num_threads && pool.get_tasks_queued() == num_threads); sem.release(static_cast(num_threads)); std::this_thread::sleep_for(sleep_time); - sync_out.println("After releasing ", num_threads, " more tasks, should have: ", num_threads, " tasks total, ", num_threads, " tasks running, ", 0, " tasks queued..."); - sync_out.print("Result: ", pool.get_tasks_total(), " tasks total, ", pool.get_tasks_running(), " tasks running, ", pool.get_tasks_queued(), " tasks queued "); + logln("After releasing ", num_threads, " more tasks, should have: ", num_threads, " tasks total, ", num_threads, " tasks running, ", 0, " tasks queued..."); + log("Result: ", pool.get_tasks_total(), " tasks total, ", pool.get_tasks_running(), " tasks running, ", pool.get_tasks_queued(), " tasks queued "); check(pool.get_tasks_total() == num_threads && pool.get_tasks_running() == num_threads && pool.get_tasks_queued() == 0); sem.release(static_cast(num_threads)); std::this_thread::sleep_for(sleep_time); - sync_out.println("After releasing the final ", num_threads, " tasks, should have: ", 0, " tasks total, ", 0, " tasks running, ", 0, " tasks queued..."); - sync_out.print("Result: ", pool.get_tasks_total(), " tasks total, ", pool.get_tasks_running(), " tasks running, ", pool.get_tasks_queued(), " tasks queued "); + logln("After releasing the final ", num_threads, " tasks, should have: ", 0, " tasks total, ", 0, " tasks running, ", 0, " tasks queued..."); + log("Result: ", pool.get_tasks_total(), " tasks total, ", pool.get_tasks_running(), " tasks running, ", pool.get_tasks_queued(), " tasks queued "); check(pool.get_tasks_total() == 0 && pool.get_tasks_running() == 0 && pool.get_tasks_queued() == 0); } @@ -1751,29 +2091,29 @@ void check_pausing() { constexpr std::chrono::milliseconds sleep_time(200); BS::pause_thread_pool pool; - sync_out.println("Checking that the pool correctly reports that it is not paused after construction..."); + logln("Checking that the pool correctly reports that it is not paused after construction..."); check(!pool.is_paused()); - sync_out.println("Pausing pool."); + logln("Pausing pool."); pool.pause(); - sync_out.println("Checking that the pool correctly reports that it is paused..."); + logln("Checking that the pool correctly reports that it is paused..."); check(pool.is_paused()); - sync_out.println("Submitting task and waiting."); + logln("Submitting task and waiting."); std::atomic flag = false; pool.detach_task( [&flag] { flag = true; - sync_out.println("Task executed."); + logln("Task executed."); }); std::this_thread::sleep_for(sleep_time); - sync_out.println("Verifying that the task has not been executed..."); + logln("Verifying that the task has not been executed..."); check(!flag); - sync_out.println("Unpausing pool and waiting."); + logln("Unpausing pool and waiting."); pool.unpause(); std::this_thread::sleep_for(sleep_time); - sync_out.println("Verifying that the task has been executed..."); + logln("Verifying that the task has been executed..."); check(flag); - sync_out.println("Checking that the pool correctly reports that it is not paused..."); + logln("Checking that the pool correctly reports that it is not paused..."); check(!pool.is_paused()); } @@ -1786,7 +2126,7 @@ void check_purge() constexpr std::chrono::milliseconds short_sleep_time(100); constexpr std::size_t num_tasks = 10; BS::thread_pool pool(1); - sync_out.println("Submitting ", num_tasks, " tasks to the pool."); + logln("Submitting ", num_tasks, " tasks to the pool."); std::vector> flags(num_tasks); for (std::size_t i = 0; i < num_tasks; ++i) { @@ -1794,15 +2134,15 @@ void check_purge() [&flags, i, long_sleep_time] { std::this_thread::sleep_for(long_sleep_time); - sync_out.println("Task ", i, " done."); + logln("Task ", i, " done."); flags[i] = true; }); } std::this_thread::sleep_for(short_sleep_time); - sync_out.println("Purging the pool and waiting for tasks..."); + logln("Purging the pool and waiting for tasks..."); pool.purge(); pool.wait(); - sync_out.println("Checking that only the first task was executed..."); + logln("Checking that only the first task was executed..."); flags[0] = !flags[0]; check(no_flags_set(flags)); } @@ -1825,7 +2165,7 @@ struct test_exception : public std::runtime_error */ void throws() { - sync_out.println("Throwing exception..."); + logln("Throwing exception..."); throw test_exception(); }; @@ -1835,7 +2175,7 @@ void throws() void check_exceptions_submit() { BS::thread_pool pool; - sync_out.println("Checking that exceptions are forwarded correctly by submit_task()..."); + logln("Checking that exceptions are forwarded correctly by submit_task()..."); bool caught = false; std::future future = pool.submit_task(throws); try @@ -1855,7 +2195,7 @@ void check_exceptions_submit() void check_exceptions_multi_future() { BS::thread_pool pool; - sync_out.println("Checking that exceptions are forwarded correctly by BS::multi_future..."); + logln("Checking that exceptions are forwarded correctly by BS::multi_future..."); bool caught = false; BS::multi_future future; future.push_back(pool.submit_task(throws)); @@ -1894,7 +2234,7 @@ bool check_vector_of_size(BS::thread_pool<>& pool, const std::size_t vector_size vector_1[i] = random(-value_range, value_range); vector_2[i] = random(-value_range, value_range); } - sync_out.println("Adding two vectors with ", vector_size, " elements using ", num_tasks, " tasks..."); + logln("Adding two vectors with ", vector_size, " elements using ", num_tasks, " tasks..."); std::vector sum_single(vector_size); for (std::size_t i = 0; i < vector_size; ++i) sum_single[i] = vector_1[i] + vector_2[i]; @@ -1946,7 +2286,7 @@ void check_priority() BS::thread_pool pool(1); pool.pause(); - // Create a shuffled lists of priorities. + // Create a shuffled list of priorities. std::vector priorities; priorities.reserve(num_tasks - 1); for (std::size_t i = 0; i < num_tasks - 1; ++i) @@ -1962,14 +2302,14 @@ void check_priority() const auto execute_task_priority = [&execution_order, &exec_mutex](const BS::priority_t priority) { const std::scoped_lock lock(exec_mutex); - sync_out.println("Task with priority ", static_cast(priority), " executed."); + logln("Task with priority ", static_cast(priority), " executed."); execution_order.push_back(priority); }; const std::vector functions = {"detach_task", "submit_task", "detach_sequence", "submit_sequence", "detach_loop", "submit_loop", "detach_blocks", "submit_blocks"}; for (const BS::priority_t priority : priorities) { const std::string_view func = functions[random(0, functions.size() - 1)]; - sync_out.println("Launching ", func, "() with priority ", static_cast(priority), "..."); + logln("Launching ", func, "() with priority ", static_cast(priority), "..."); if (func == "detach_task") { pool.detach_task( @@ -2051,7 +2391,7 @@ void check_priority() } // Unpause the pool so the tasks can be executed, then check that they were executed in the correct order. - sync_out.println("Checking execution order..."); + logln("Checking execution order..."); std::this_thread::sleep_for(sleep_time); pool.unpause(); pool.wait(); @@ -2068,7 +2408,7 @@ void check_priority() */ void check_init() { - sync_out.println("Comparing thread indices reported by get_index() using an initialization function passed to reset():"); + logln("Comparing thread indices reported by get_index() using an initialization function passed to reset():"); std::vector> thread_indices(std::thread::hardware_concurrency()); std::atomic correct = true; BS::thread_pool pool; @@ -2082,7 +2422,7 @@ void check_init() correct = false; }); pool.wait(); - sync_out.println("Checking that all reported indices have values..."); + logln("Checking that all reported indices have values..."); check(correct); correct = true; for (std::size_t i = 0; i < thread_indices.size(); ++i) @@ -2093,14 +2433,14 @@ void check_init() break; } } - sync_out.println("Checking that all reported indices are correct..."); + logln("Checking that all reported indices are correct..."); check(correct); - sync_out.println("Verifying that the index of the main thread has no value..."); + logln("Verifying that the index of the main thread has no value..."); const std::optional main_idx = BS::this_thread::get_index(); check(!main_idx.has_value()); - sync_out.println("Verifying that the index of an independent thread has no value..."); + logln("Verifying that the index of an independent thread has no value..."); std::thread test_thread( [] { @@ -2115,7 +2455,7 @@ void check_init() */ void check_cleanup() { - sync_out.println("Comparing thread indices reported by get_index() using a cleanup function passed to set_cleanup_func():"); + logln("Comparing thread indices reported by get_index() using a cleanup function passed to set_cleanup_func():"); std::vector> thread_indices(std::thread::hardware_concurrency()); std::atomic correct = true; { @@ -2130,7 +2470,7 @@ void check_cleanup() correct = false; }); } - sync_out.println("Checking that all reported indices have values..."); + logln("Checking that all reported indices have values..."); check(correct); correct = true; for (std::size_t i = 0; i < thread_indices.size(); ++i) @@ -2141,7 +2481,7 @@ void check_cleanup() break; } } - sync_out.println("Checking that all reported indices are correct..."); + logln("Checking that all reported indices are correct..."); check(correct); } @@ -2150,7 +2490,7 @@ void check_cleanup() */ void check_get_pool() { - sync_out.println("Checking that all threads report the correct pool..."); + logln("Checking that all threads report the correct pool..."); std::vector> thread_pool_ptrs1(std::thread::hardware_concurrency()); std::vector> thread_pool_ptrs2(std::thread::hardware_concurrency()); const auto store_pointers = [](std::vector>& ptrs) @@ -2180,12 +2520,12 @@ void check_get_pool() check_pointers(thread_pool_ptrs1, pool1); check_pointers(thread_pool_ptrs2, pool2); { - sync_out.println("Verifying that the pool pointer of the main thread has no value..."); + logln("Verifying that the pool pointer of the main thread has no value..."); const auto ptr = BS::this_thread::get_pool(); check(!ptr.has_value()); } { - sync_out.println("Verifying that the pool pointer of an independent thread has no value..."); + logln("Verifying that the pool pointer of an independent thread has no value..."); std::thread test_thread( [] { @@ -2236,7 +2576,7 @@ void check_copy(const std::string_view which_func) { BS::thread_pool pool; const std::size_t num_tasks = pool.get_thread_count() * 10; - sync_out.println("Checking ", which_func, "..."); + logln("Checking ", which_func, "..."); std::atomic copied = 0; std::atomic moved = 0; auto task = [detect = count_copy_move(&copied, &moved)](auto&&...) {}; @@ -2253,7 +2593,7 @@ void check_copy(const std::string_view which_func) else if (which_func == "submit_sequence()") std::ignore = pool.submit_sequence(0, num_tasks, std::move(task)); pool.wait(); - sync_out.println("Copy count: "); + logln("Copy count: "); check(0, copied.load()); // Note: Move count will be unpredictable if priority is on, so we don't check it. } @@ -2308,7 +2648,7 @@ void check_shared_ptr(const std::string_view which_func) std::atomic object_exists = false; std::atomic uses_before_destruct = 0; std::atomic uses_after_destruct = 0; - sync_out.println("Checking ", which_func, "..."); + logln("Checking ", which_func, "..."); { std::shared_ptr ptr = std::make_shared(&object_exists); auto task = [ptr, &object_exists, &uses_before_destruct, &uses_after_destruct, &sleep_time](auto&&...) @@ -2335,9 +2675,9 @@ void check_shared_ptr(const std::string_view which_func) } pool.wait(); std::this_thread::sleep_for(sleep_time); - sync_out.println("Uses before destruct:"); + logln("Uses before destruct:"); check(num_tasks, uses_before_destruct.load()); - sync_out.println("Uses after destruct:"); + logln("Uses after destruct:"); check(0, uses_after_destruct.load()); } @@ -2376,21 +2716,21 @@ void check_task_destruct() void check_common_index_type() { // NOLINTBEGIN(misc-redundant-expression) - sync_out.println("Checking std::int8_t..."); + logln("Checking std::int8_t..."); check(std::is_same_v, std::int8_t> && std::is_same_v, std::int16_t> && std::is_same_v, std::int32_t> && std::is_same_v, std::int64_t> && std::is_same_v, std::int16_t> && std::is_same_v, std::int32_t> && std::is_same_v, std::int64_t> && std::is_same_v, std::uint64_t>); - sync_out.println("Checking std::int16_t..."); + logln("Checking std::int16_t..."); check(std::is_same_v, std::int16_t> && std::is_same_v, std::int16_t> && std::is_same_v, std::int32_t> && std::is_same_v, std::int64_t> && std::is_same_v, std::int16_t> && std::is_same_v, std::int32_t> && std::is_same_v, std::int64_t> && std::is_same_v, std::uint64_t>); - sync_out.println("Checking std::int32_t..."); + logln("Checking std::int32_t..."); check(std::is_same_v, std::int32_t> && std::is_same_v, std::int32_t> && std::is_same_v, std::int32_t> && std::is_same_v, std::int64_t> && std::is_same_v, std::int32_t> && std::is_same_v, std::int32_t> && std::is_same_v, std::int64_t> && std::is_same_v, std::uint64_t>); - sync_out.println("Checking std::int64_t..."); + logln("Checking std::int64_t..."); check(std::is_same_v, std::int64_t> && std::is_same_v, std::int64_t> && std::is_same_v, std::int64_t> && std::is_same_v, std::int64_t> && std::is_same_v, std::int64_t> && std::is_same_v, std::int64_t> && std::is_same_v, std::int64_t> && std::is_same_v, std::uint64_t>); - sync_out.println("Checking std::uint8_t..."); + logln("Checking std::uint8_t..."); check(std::is_same_v, std::int16_t> && std::is_same_v, std::int16_t> && std::is_same_v, std::int32_t> && std::is_same_v, std::int64_t> && std::is_same_v, std::uint8_t> && std::is_same_v, std::uint16_t> && std::is_same_v, std::uint32_t> && std::is_same_v, std::uint64_t>); - sync_out.println("Checking std::uint16_t..."); + logln("Checking std::uint16_t..."); check(std::is_same_v, std::int32_t> && std::is_same_v, std::int32_t> && std::is_same_v, std::int32_t> && std::is_same_v, std::int64_t> && std::is_same_v, std::uint16_t> && std::is_same_v, std::uint16_t> && std::is_same_v, std::uint32_t> && std::is_same_v, std::uint64_t>); - sync_out.println("Checking std::uint32_t..."); + logln("Checking std::uint32_t..."); check(std::is_same_v, std::int64_t> && std::is_same_v, std::int64_t> && std::is_same_v, std::int64_t> && std::is_same_v, std::int64_t> && std::is_same_v, std::uint32_t> && std::is_same_v, std::uint32_t> && std::is_same_v, std::uint32_t> && std::is_same_v, std::uint64_t>); - sync_out.println("Checking std::uint64_t..."); + logln("Checking std::uint64_t..."); check(std::is_same_v, std::uint64_t> && std::is_same_v, std::uint64_t> && std::is_same_v, std::uint64_t> && std::is_same_v, std::uint64_t> && std::is_same_v, std::uint64_t> && std::is_same_v, std::uint64_t> && std::is_same_v, std::uint64_t> && std::is_same_v, std::uint64_t>); // NOLINTEND(misc-redundant-expression) } @@ -2428,17 +2768,17 @@ void check_deadlock(const F&& task) check_deadlock_pool.wait_for(sleep_time); if (try_n == tries) { - sync_out.println("Successfully finished all tries!"); + logln_ansi(ansi_success, "Successfully finished all tries!"); passed = true; break; } if (try_n == old_try_n) { - sync_out.println("Error: deadlock detected!"); + logln_ansi(ansi_error, "Error: deadlock detected!"); passed = false; break; } - sync_out.println("Finished ", try_n, " tries out of ", tries, "..."); + logln("Finished ", try_n, " tries out of ", tries, "..."); } check(passed); } @@ -2499,24 +2839,25 @@ void check_root(const bool condition) { if (condition) { - sync_out.println("-> passed."); + logln("-> passed."); ++test_results::tests_succeeded; } else { - sync_out.println("-> failed, most likely due to insufficient permissions; ignoring."); + logln("-> failed, most likely due to insufficient permissions; ignoring."); } } /** * @brief Check if the expected result has been obtained, report the result, but do not keep count of the total number of successes and failures, because failure is expected if the test is not run as root. * - * @param condition The condition to check. + * @param expected The expected result. + * @param obtained The obtained result. */ template void check_root(const T1& expected, const T2& obtained) { - sync_out.print("- Expected: ", expected, ", obtained: ", obtained, ' '); + log("- Expected: ", expected, ", obtained: ", obtained, ' '); check_root(expected == static_cast(obtained)); } @@ -2525,13 +2866,13 @@ void check_root(const T1& expected, const T2& obtained) */ void check_os_process_priorities() { - sync_out.println("Checking OS process priorities..."); - sync_out.println("NOTE: This test must be run as admin/root, otherwise it will fail!"); + logln("Checking OS process priorities..."); + logln_ansi(ansi_info, "NOTE: This test must be run as admin/root, otherwise it will fail!"); // We go over the priorities in reverse order because on Linux, a non-root user can only decrease the priority, so if we start from the lowest priority, all tests will fail except the first one. const std::vector priorities = {BS::os_process_priority::realtime, BS::os_process_priority::high, BS::os_process_priority::above_normal, BS::os_process_priority::normal, BS::os_process_priority::below_normal, BS::os_process_priority::idle}; for (BS::os_process_priority priority : priorities) { - sync_out.print("Setting OS process priority to ", os_process_priority_name(priority), ' '); + log("Setting OS process priority to ", os_process_priority_name(priority), ' '); // On Windows we should be able to set all the priorities even as non-admin; realtime will "succeed" but actually set the priority to high. On Linux, only root can increase the priority beyond normal. #ifdef _WIN32 check(BS::set_os_process_priority(priority)); @@ -2542,7 +2883,7 @@ void check_os_process_priorities() check_root(BS::set_os_process_priority(priority)); #endif const std::optional new_priority = BS::get_os_process_priority(); - sync_out.print("Obtaining new OS process priority "); + log("Obtaining new OS process priority "); check(new_priority.has_value()); #ifdef _WIN32 if (priority != BS::os_process_priority::realtime) @@ -2557,7 +2898,7 @@ void check_os_process_priorities() #endif } // Set the priority back to normal after the test ends. This will fail on Linux if not root. - sync_out.println("Setting priority back to normal..."); + logln("Setting priority back to normal..."); #ifdef _WIN32 check(BS::set_os_process_priority(BS::os_process_priority::normal)); #else @@ -2574,14 +2915,14 @@ void check_os_thread_priorities() pool.detach_task( [] { - sync_out.println("Checking OS thread priorities for pool threads..."); + logln("Checking OS thread priorities for pool threads..."); #ifdef __linux__ - sync_out.println("NOTE: On Linux, this test must be run as root, otherwise it will fail!"); + logln_ansi(ansi_info, "NOTE: On Linux, this test must be run as root, otherwise it will fail!"); #endif const std::vector priorities = {BS::os_thread_priority::realtime, BS::os_thread_priority::highest, BS::os_thread_priority::above_normal, BS::os_thread_priority::normal, BS::os_thread_priority::below_normal, BS::os_thread_priority::lowest, BS::os_thread_priority::idle}; for (BS::os_thread_priority priority : priorities) { - sync_out.print("Setting OS thread priority to ", os_thread_priority_name(priority), ' '); + log("Setting OS thread priority to ", os_thread_priority_name(priority), ' '); // On Windows we should be able to set all the priorities even as non-admin, including realtime. On Linux, only root can increase the priority beyond normal. (Also, note that on WSL, even root cannot set the priority to highest or above.) #ifdef _WIN32 check(BS::this_thread::set_os_thread_priority(priority)); @@ -2592,7 +2933,7 @@ void check_os_thread_priorities() check_root(BS::this_thread::set_os_thread_priority(priority)); #endif const std::optional new_priority = BS::this_thread::get_os_thread_priority(); - sync_out.print("Obtaining new OS thread priority "); + log("Obtaining new OS thread priority "); check(new_priority.has_value()); #ifdef _WIN32 check(os_thread_priority_name(priority), os_thread_priority_name(new_priority)); @@ -2601,7 +2942,7 @@ void check_os_thread_priorities() #endif } // Set the priority back to normal after the test ends. This will fail on Linux/macOS if not running as root. - sync_out.println("Setting priority back to normal..."); + logln("Setting priority back to normal..."); #ifdef _WIN32 check(BS::this_thread::set_os_thread_priority(BS::os_thread_priority::normal)); #else @@ -2615,11 +2956,11 @@ void check_os_thread_priorities() */ void check_os_thread_names() { - sync_out.println("Checking OS thread names..."); + logln("Checking OS thread names..."); const std::string name = "BS_thread_pool"; - sync_out.println("Setting main thread name to \"", name, "\"..."); + logln("Setting main thread name to \"", name, "\"..."); check(BS::this_thread::set_os_thread_name(name)); - sync_out.println("Obtaining new OS thread name..."); + logln("Obtaining new OS thread name..."); std::optional new_name = BS::this_thread::get_os_thread_name(); if (new_name.has_value()) { @@ -2632,6 +2973,7 @@ void check_os_thread_names() } } + #if defined(_WIN32) || defined(__linux__) /** * @brief Convert a `std::vector` representing CPU affinity to a string of 0s and 1s. * @@ -2656,38 +2998,38 @@ std::string affinity_to_string(const std::optional>& affinity) */ void check_os_process_affinity() { - sync_out.println("Checking OS process affinity..."); + logln("Checking OS process affinity..."); - sync_out.print("Obtaining initial process affinity "); + log("Obtaining initial process affinity "); const std::optional> initial_affinity = BS::get_os_process_affinity(); check(initial_affinity.has_value()); - sync_out.println("Initial affinity is: ", affinity_to_string(initial_affinity)); + logln("Initial affinity is: ", affinity_to_string(initial_affinity)); const std::size_t num_bits = initial_affinity.has_value() ? initial_affinity->size() : std::thread::hardware_concurrency(); - sync_out.print("Setting affinity to CPU 1 only "); + log("Setting affinity to CPU 1 only "); std::vector cpu_1_in(num_bits, false); cpu_1_in[0] = true; check(BS::set_os_process_affinity(cpu_1_in)); - sync_out.print("Obtaining new affinity "); + log("Obtaining new affinity "); const std::optional> cpu_1_out = BS::get_os_process_affinity(); check(cpu_1_out.has_value()); check(affinity_to_string(cpu_1_in), affinity_to_string(cpu_1_out)); - sync_out.print("Setting affinity to alternating CPUs "); + log("Setting affinity to alternating CPUs "); std::vector alternating_in(num_bits, false); for (std::size_t i = 0; i < num_bits; ++i) alternating_in[i] = (i % 2 == 1); check(BS::set_os_process_affinity(alternating_in)); - sync_out.print("Obtaining new affinity "); + log("Obtaining new affinity "); const std::optional> alternating_out = BS::get_os_process_affinity(); check(alternating_out.has_value()); check(affinity_to_string(alternating_in), affinity_to_string(alternating_out)); if (initial_affinity.has_value()) { - sync_out.print("Setting affinity back to initial value "); + log("Setting affinity back to initial value "); check(BS::set_os_process_affinity(*initial_affinity)); - sync_out.print("Obtaining new affinity "); + log("Obtaining new affinity "); const std::optional> initial_out = BS::get_os_process_affinity(); check(initial_out.has_value()); check(affinity_to_string(initial_affinity), affinity_to_string(initial_out)); @@ -2709,38 +3051,38 @@ void check_os_thread_affinity() const std::vector all_enabled(num_process_bits, true); BS::set_os_process_affinity(all_enabled); - sync_out.println("Checking OS thread affinity for pool threads..."); + logln("Checking OS thread affinity for pool threads..."); - sync_out.print("Obtaining initial thread affinity "); + log("Obtaining initial thread affinity "); const std::optional> initial_affinity = BS::this_thread::get_os_thread_affinity(); check(initial_affinity.has_value()); - sync_out.println("Initial affinity is: ", affinity_to_string(initial_affinity)); + logln("Initial affinity is: ", affinity_to_string(initial_affinity)); const std::size_t num_bits = initial_affinity.has_value() ? initial_affinity->size() : std::thread::hardware_concurrency(); - sync_out.print("Setting affinity to CPU 1 only "); + log("Setting affinity to CPU 1 only "); std::vector cpu_1_in(num_bits, false); cpu_1_in[0] = true; check(BS::this_thread::set_os_thread_affinity(cpu_1_in)); - sync_out.print("Obtaining new affinity "); + log("Obtaining new affinity "); const std::optional> cpu_1_out = BS::this_thread::get_os_thread_affinity(); check(cpu_1_out.has_value()); check(affinity_to_string(cpu_1_in), affinity_to_string(cpu_1_out)); - sync_out.print("Setting affinity to alternating CPUs "); + log("Setting affinity to alternating CPUs "); std::vector alternating_in(num_bits, false); for (std::size_t i = 0; i < num_bits; ++i) alternating_in[i] = (i % 2 == 1); check(BS::this_thread::set_os_thread_affinity(alternating_in)); - sync_out.print("Obtaining new affinity "); + log("Obtaining new affinity "); const std::optional> alternating_out = BS::this_thread::get_os_thread_affinity(); check(alternating_out.has_value()); check(affinity_to_string(alternating_in), affinity_to_string(alternating_out)); if (initial_affinity.has_value()) { - sync_out.print("Setting affinity back to initial value "); + log("Setting affinity back to initial value "); check(BS::this_thread::set_os_thread_affinity(*initial_affinity)); - sync_out.print("Obtaining new affinity "); + log("Obtaining new affinity "); const std::optional> initial_out = BS::this_thread::get_os_thread_affinity(); check(initial_out.has_value()); check(affinity_to_string(initial_affinity), affinity_to_string(initial_out)); @@ -2750,6 +3092,7 @@ void check_os_thread_affinity() BS::set_os_process_affinity(*initial_process_affinity); }); } + #endif /** * @brief Try to set the OS priority of this thread to the highest possible value. Also set the name of the thread for debugging purposes. @@ -2793,7 +3136,7 @@ void print_timing(const mean_sd& stats, const double pixels_per_ms) constexpr int width_mean = 6; constexpr int width_sd = 4; constexpr int width_pms = 7; - sync_out.println("-> Mean: ", std::setw(width_mean), stats.mean, " ms, standard deviation: ", std::setw(width_sd), stats.sd, " ms, speed: ", std::setw(width_pms), pixels_per_ms, " pixels/ms."); + logln("-> Mean: ", std::setw(width_mean), stats.mean, " ms, standard deviation: ", std::setw(width_sd), stats.sd, " ms, speed: ", std::setw(width_pms), pixels_per_ms, " pixels/ms."); } /** @@ -2813,13 +3156,14 @@ std::size_t min_element_index(const std::vector& vec) * @brief Calculate and print the speedup obtained by multithreading. * * @param timings A vector of the timings corresponding to different numbers of tasks. + * @param try_tasks A vector containing the numbers of tasks tried. */ void print_speedup(const std::vector& timings, const std::vector& try_tasks) { const std::size_t min_el = min_element_index(timings); const double max_speedup = std::round((timings[0] / timings[min_el]) * 10) / 10; const std::size_t num_tasks = try_tasks[min_el]; - sync_out.println("Maximum speedup obtained by multithreading vs. single-threading: ", max_speedup, "x, using ", num_tasks, " tasks."); + logln("Maximum speedup obtained by multithreading vs. single-threading: ", max_speedup, "x, using ", num_tasks, " tasks."); } /** @@ -2830,13 +3174,18 @@ void print_speedup(const std::vector& timings, const std::vector& timings) { + // First, calculate the mean and the mean of the square . double mean = 0; - for (std::size_t i = 0; i < timings.size(); ++i) - mean += static_cast(timings[i]) / static_cast(timings.size()); - double variance = 0; - for (std::size_t i = 0; i < timings.size(); ++i) - variance += (static_cast(timings[i]) - mean) * (static_cast(timings[i]) - mean) / static_cast(timings.size()); - return {mean, std::sqrt(variance)}; + double mean_sq = 0; + for (const std::chrono::milliseconds::rep timing : timings) + { + mean += static_cast(timing); + mean_sq += static_cast(timing * timing); + } + mean /= static_cast(timings.size()); + mean_sq /= static_cast(timings.size()); + // The variance is given by <(X - )^2> = - ^2. The standard deviation is the square root of the variance. + return {mean, std::sqrt(mean_sq - (mean * mean))}; } /** @@ -2918,10 +3267,10 @@ double mandelbrot_escape(const std::complex c) */ struct [[nodiscard]] color { - color() = default; + constexpr color() = default; template - color(const T r_, const T g_, const T b_) : r(static_cast(r_)), g(static_cast(g_)), b(static_cast(b_)) + constexpr color(const T r_, const T g_, const T b_) : r(static_cast(r_)), g(static_cast(g_)), b(static_cast(b_)) { } @@ -2951,8 +3300,8 @@ color interpolate_colors(const color& first, const color& second, const double t */ color iter_to_color(const double iterations) { - // Define a nice color pallette for the image. - static const std::vector palette = {{66, 30, 15}, {25, 7, 26}, {9, 1, 47}, {4, 4, 73}, {0, 7, 100}, {12, 44, 138}, {24, 82, 177}, {57, 125, 209}, {134, 181, 229}, {211, 236, 248}, {241, 233, 191}, {248, 201, 95}, {255, 170, 0}, {204, 128, 0}, {153, 87, 0}, {106, 52, 3}}; + // Define a nice color palette for the image. + static constexpr std::array palette = {{{66, 30, 15}, {25, 7, 26}, {9, 1, 47}, {4, 4, 73}, {0, 7, 100}, {12, 44, 138}, {24, 82, 177}, {57, 125, 209}, {134, 181, 229}, {211, 236, 248}, {241, 233, 191}, {248, 201, 95}, {255, 170, 0}, {204, 128, 0}, {153, 87, 0}, {106, 52, 3}}}; // Points that are in the set (or at least, suspected to be in the set because they did not diverge after the maximum number of iterations) will be black. if (iterations == max_iter) @@ -3012,7 +3361,6 @@ void calculate_mandelbrot(image_matrix& image, const std::size_t start, c * * @param image The matrix containing the pixels. * @param filename The output file name. - * @return `true` if the file was saved successfully, `false` otherwise. */ void save_bmp(const image_matrix& image, const std::string& filename) { @@ -3020,11 +3368,11 @@ void save_bmp(const image_matrix& image, const std::string& filename) std::ofstream file(filename, std::ios::binary); if (!file.is_open()) { - sync_out.println("Error: Could not create the file ", filename, '.'); + logln_ansi(ansi_error, "Error: Could not create the file ", filename, '.'); return; } - sync_out.print("Saving image to a BMP file: ["); + log("Saving image to a BMP file: ["); // Calculate the size of the BMP file in bytes. const std::uint32_t width = static_cast(image.get_width()); @@ -3067,11 +3415,11 @@ void save_bmp(const image_matrix& image, const std::string& filename) file.write(reinterpret_cast(padding_bytes), num_padding_bytes); } if (y % (height / 10) == 0) - sync_out.print('.'); + log('.'); } file.close(); - sync_out.println("]\nMandelbrot image saved successfully as ", filename, '.'); + logln("]\nMandelbrot image saved successfully as ", filename, '.'); } /** @@ -3128,6 +3476,119 @@ private: std::chrono::duration elapsed_time = std::chrono::duration::zero(); }; // class timer +/** + * @brief Map a color to a monochrome Unicode block based on its brightness, using luma coefficients. + * + * @param col The color. + * @return The Unicode block representing the brightness of the color, in UTF-8. + */ +std::string_view brightness_block(const color& col) +{ + // Define Unicode blocks from darkest to brightest in UTF-8. + constexpr std::array blocks = {" ", "\xE2\x96\x91", "\xE2\x96\x92", "\xE2\x96\x93", "\xE2\x96\x88"}; + // Compute the perceived brightness using luma coefficients. Each color component is a number in the range 0-255, and the coefficients sum to 1, so the brightness is also in the range 0-255. + const double brightness = (0.2126 * col.r) + (0.7152 * col.g) + (0.0722 * col.b); + // Quantize the brightness into 5 levels. A brightness of 0 maps to level 0, and a brightness of 255 maps to level 4. + const std::size_t level = static_cast(std::round(brightness * 4.0 / 255.0)); + // Return the corresponding block character. + return blocks[level]; +} + +/** + * @brief Create a plot of an image as characters, using either 24-bit ANSI colors or monochrome blocks of different brightness. + * + * @param image The image to plot. + * @param out_width The plot width in terminal characters. Should be even, since in the monochrome case each pixel spans two characters horizontally. + * @param use_color `true` to generate a colored plot, `false` to generate a monochrome plot. + * @return The plot as a string. + */ +std::string plot_image_chars(const image_matrix& image, const std::size_t out_width, const bool use_color) +{ + // Get the source image dimensions. + const std::size_t src_width = image.get_width(); + const std::size_t src_height = image.get_height(); + // Compute the plot height in terminal characters, keeping in mind that characters have an aspect ratio of about 1:2 (width:height). + const std::size_t out_height = static_cast(std::llround(0.5 * static_cast(src_height) * static_cast(out_width) / static_cast(src_width))); + // Create a buffer for the plot. + std::string plot; + + // For the colored plot we use Unicode lower half blocks to pack two pixels per character. The background color sets the color of the top (empty) half, and the foreground color sets the color of the bottom (filled) half. + if (use_color) + { + // Each character row contains two pixel rows (top and bottom). + const std::size_t out_height_pixels = out_height * 2; + // Create a mapping of output x coordinates to source pixels, such that x_map[0] = 0 and x_map[out_width - 1] = src_width - 1. + std::vector x_map(out_width); + for (std::size_t x = 0; x < out_width; ++x) + x_map[x] = (x * (src_width - 1)) / (out_width - 1); + // Create a mapping of output pixel y coordinates to source pixels, such that y_map[0] = 0 and y_map[out_height_pixels - 1] = src_height - 1. + std::vector y_map(out_height_pixels); + for (std::size_t y = 0; y < out_height_pixels; ++y) + y_map[y] = (y * (src_height - 1)) / (out_height_pixels - 1); + // Reserve enough capacity for the entire plot to avoid reallocations. Each row contains ANSI codes of the form `\033[48;2;RRR;GGG;BBBm\033[38;2;RRR;GGG;BBBm` = up to 38 bytes, plus the Unicode lower half block U+2584 in UTF-8 = 3 bytes, for a total of 41 bytes per character of the plot, plus the `\033[0m` reset code at the end and the newline = 5 bytes. + plot.reserve(out_height * ((out_width * 41) + 5)); + // Iterate over the rows and columns. + for (std::size_t y = 0; y < out_height; ++y) + { + for (std::size_t x = 0; x < out_width; ++x) + { + // Fetch the sampled source pixel for the top and bottom halves. + const color col_top = image(x_map[x], y_map[2 * y]); + const color col_bottom = image(x_map[x], y_map[(2 * y) + 1]); + // Create the background color ANSI sequence for the top half. + plot.append("\033[48;2;"); + plot.append(std::to_string(col_top.r)); + plot.push_back(';'); + plot.append(std::to_string(col_top.g)); + plot.push_back(';'); + plot.append(std::to_string(col_top.b)); + // Finish the sequence with `m` and create the foreground color ANSI sequence for the bottom half. + plot.append("m\033[38;2;"); + plot.append(std::to_string(col_bottom.r)); + plot.push_back(';'); + plot.append(std::to_string(col_bottom.g)); + plot.push_back(';'); + plot.append(std::to_string(col_bottom.b)); + // Finish the sequence with `m` and append a lower half block (U+2584) in UTF-8, which is the actual character that will be displayed. + plot.append("m\xE2\x96\x84"); + } + // Reset the ANSI style at the end of the row and add a newline character. + plot.append("\033[0m\n"); + } + } + // For the monochrome plot we use Unicode block characters of different brightness levels. Each pixel is rendered as two identical blocks side-by-side to make the pixels square (so essentially, the opposite of the colored plot). + else + { + // Each pixel spans two terminal characters horizontally. + const std::size_t out_width_pixels = out_width / 2; + // Create a mapping of output x coordinates to source pixels, such that x_map[0] = 0 and x_map[out_width_pixels - 1] = src_width - 1. + std::vector x_map(out_width_pixels); + for (std::size_t x = 0; x < out_width_pixels; ++x) + x_map[x] = (x * (src_width - 1)) / (out_width_pixels - 1); + // Create a mapping of output y coordinates to source pixels, such that y_map[0] = 0 and y_map[out_height - 1] = src_height - 1. + std::vector y_map(out_height); + for (std::size_t y = 0; y < out_height; ++y) + y_map[y] = (y * (src_height - 1)) / (out_height - 1); + // Reserve enough capacity for the entire plot to avoid reallocations. The UTF-8 blocks can take up to 3 bytes each, plus a newline after each row. + plot.reserve(out_height * ((out_width * 3) + 1)); + // Iterate over the rows and columns. + for (std::size_t y = 0; y < out_height; ++y) + { + for (std::size_t x = 0; x < out_width_pixels; ++x) + { + // Fetch the sampled source pixel and map to a Unicode block based on brightness. + const color col = image(x_map[x], y_map[y]); + const std::string_view block = brightness_block(col); + plot.append(block); + plot.append(block); + } + // Add a newline after each row. + plot.push_back('\n'); + } + } + return plot; +} + /** * @brief Benchmark multithreaded performance by calculating the Mandelbrot set. * @@ -3146,15 +3607,15 @@ void check_performance(const bool benchmark, const bool plot, const bool save) BS::set_os_process_priority(BS::os_process_priority::above_normal); const std::string process_priority = os_process_priority_name(BS::get_os_process_priority()); - sync_out.println("Process priority set to: ", process_priority, "."); + logln("Process priority set to: ", process_priority, "."); if (process_priority != "realtime") - sync_out.println("Note: Please run as admin/root to enable a higher process priority."); + logln_ansi(ansi_info, "Note: Please run as admin/root to enable a higher process priority."); try_os_thread_priority(); const std::string thread_priority = os_thread_priority_name(BS::this_thread::get_os_thread_priority()); - sync_out.println("Thread priority set to: ", thread_priority, "."); + logln("Thread priority set to: ", thread_priority, "."); if (thread_priority != "realtime") - sync_out.println("Note: Please run as admin/root to enable a higher thread priority."); + logln_ansi(ansi_info, "Note: Please run as admin/root to enable a higher thread priority."); // Initialize a thread pool with the default number of threads, and ensure that the threads have the highest possible priority, so that other processes do not interfere with the benchmarks. BS::thread_pool pool(try_os_thread_priority); @@ -3165,10 +3626,10 @@ void check_performance(const bool benchmark, const bool plot, const bool save) // Store the number of available hardware threads for easy access. const std::size_t thread_count = pool.get_thread_count(); - sync_out.println("Using ", thread_count, " threads."); + logln("Using ", thread_count, " threads."); // Set the formatting of floating point numbers. - sync_out.print(std::fixed, std::setprecision(1)); + log(std::fixed, std::setprecision(1)); // Initialize a timer object to measure execution time. timer tmr; @@ -3177,7 +3638,7 @@ void check_performance(const bool benchmark, const bool plot, const bool save) constexpr std::chrono::milliseconds::rep target_ms = 50; // Find the Mandelbrot image size that will roughly achieve the target execution time. - sync_out.println("Determining the Mandelbrot image size needed to achieve an approximate mean execution time of ", target_ms, " ms with ", thread_count, " tasks..."); + logln("Determining the Mandelbrot image size needed to achieve an approximate mean execution time of ", target_ms, " ms with ", thread_count, " tasks..."); std::size_t image_size = thread_count; image_matrix image; std::size_t jump = 1; @@ -3202,7 +3663,7 @@ void check_performance(const bool benchmark, const bool plot, const bool save) // Scale the image size to fit the target execution time more precisely, keeping in mind that the time complexity is O(image_size^2). image_size = static_cast(std::llround(static_cast(image_size) * std::sqrt(static_cast(target_ms) / static_cast(tmr.ms())))); - sync_out.println("Result: ", image_size, 'x', image_size, " pixels."); + logln("Result: ", image_size, 'x', image_size, " pixels."); if (benchmark) { @@ -3217,8 +3678,8 @@ void check_performance(const bool benchmark, const bool plot, const bool save) // Since we are repeating the same test multiple times, we might as well use different parts of the complex plane in each repetition. However, we have to spread the calculations evenly to avoid biasing the results, as some regions have much higher escape times than others. So we calculate the whole image, but at an offset from 0 to `num_repeats`. jump = num_repeats; const std::size_t benchmark_image_size = static_cast(std::floor(static_cast(image_size) * std::sqrt(num_repeats))); - sync_out.println("Generating a ", benchmark_image_size, 'x', benchmark_image_size, " plot of the Mandelbrot set..."); - sync_out.println("Each test will be repeated ", num_repeats, " times to collect reliable statistics."); + logln("Generating a ", benchmark_image_size, 'x', benchmark_image_size, " plot of the Mandelbrot set..."); + logln("Each test will be repeated ", num_repeats, " times to collect reliable statistics."); // Perform the test. std::vector try_tasks; @@ -3230,10 +3691,10 @@ void check_performance(const bool benchmark, const bool plot, const bool save) image = image_matrix(benchmark_image_size, benchmark_image_size); try_tasks.push_back(num_tasks); if (num_tasks == 0) - sync_out.print(std::setw(width_tasks), 1, " task: "); + log(std::setw(width_tasks), 1, " task: "); else - sync_out.print(std::setw(width_tasks), num_tasks, " tasks: "); - sync_out.print('['); + log(std::setw(width_tasks), num_tasks, " tasks: "); + log('[', BS::synced_stream::flush); for (std::size_t i = 0; i < num_repeats; ++i) { // Measure execution time for this test. @@ -3251,11 +3712,11 @@ void check_performance(const bool benchmark, const bool plot, const bool save) // Save the measurement for later analysis. same_n_timings.push_back(tmr.ms()); // Print a dot to inform the user that we've made progress. - sync_out.print('.'); + log('.', BS::synced_stream::flush); // Increase the offset so we calculate a different part of the image in each repetition of the test. offset = (offset + 1) % num_repeats; } - sync_out.println(']', (num_tasks == 0) ? " (single-threaded)" : ""); + logln(']', (num_tasks == 0) ? " (single-threaded)" : ""); // Analyze, print, and save the mean and standard deviation of all the tests with the same number of tasks. const mean_sd stats = analyze(same_n_timings); const std::chrono::milliseconds::rep total_time = std::reduce(same_n_timings.begin(), same_n_timings.end()); @@ -3285,22 +3746,39 @@ void check_performance(const bool benchmark, const bool plot, const bool save) constexpr std::chrono::milliseconds::rep total_ms = 5000; const std::size_t plot_image_size = static_cast(std::floor(static_cast(image_size) * std::sqrt(static_cast(total_ms) / static_cast(target_ms)))); image = image_matrix(plot_image_size, plot_image_size); - sync_out.print("Generating a ", plot_image_size, 'x', plot_image_size, " plot of the Mandelbrot set with ", thread_count, " tasks: ["); + log("Generating a ", plot_image_size, 'x', plot_image_size, " plot of the Mandelbrot set with ", thread_count, " tasks: [", BS::synced_stream::flush); pool.detach_blocks(0, plot_image_size * plot_image_size, [&image](const std::size_t start, const std::size_t end) { calculate_mandelbrot(image, start, end, 1, 0); - sync_out.print('.'); + log('.', BS::synced_stream::flush); }); pool.wait(); tmr.stop(); - sync_out.println("]\nDone in ", tmr.ms(), " ms (", static_cast(plot_image_size * plot_image_size) / static_cast(tmr.ms()), " pixels/ms)."); + logln("]\nDone in ", tmr.ms(), " ms (", static_cast(plot_image_size * plot_image_size) / static_cast(tmr.ms()), " pixels/ms)."); } + logln(); + // Set the plot width in terminal characters. + constexpr std::size_t plot_width = 120; + // Generate a colored or monochrome plot as needed. + std::string plot_color; + std::string plot_mono; + if (use_stdout && !no_color) + plot_color = plot_image_chars(image, plot_width, true); + if (use_log || (use_stdout && no_color)) + plot_mono = plot_image_chars(image, plot_width, false); + // Write the plots to stdout and/or the log file. + if (use_stdout) + sync_cout.print(no_color ? plot_mono : plot_color); + if (use_log) + sync_log.print(plot_mono); + + // Save the plot to a BMP file if requested. if (save) save_bmp(image, "BS_thread_pool_benchmark_mandelbrot.bmp"); - print_header("Thread pool performance test completed!", '+'); + print_header("Thread pool performance test completed successfully!", '+', ansi_success); } // ================================== @@ -3312,7 +3790,7 @@ void check_performance(const bool benchmark, const bool plot, const bool save) */ void show_intro() { - sync_out.println(R"( + logln_ansi(ansi_title, R"( ██████ ███████ ████████ ██ ██ ██████ ███████ █████ ██████ ██████ ██████ ██████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██████ ███████ ██ ███████ ██████ █████ ███████ ██ ██ ██████ ██ ██ ██ ██ ██ @@ -3320,32 +3798,30 @@ void show_intro() ██████ ███████ ██ ██ ██ ██ ██ ███████ ██ ██ ██████ ███████ ██ ██████ ██████ ███████ )"); - sync_out.println("BS::thread_pool: a fast, lightweight, modern, and easy-to-use C++17/C++20/C++23 thread pool library"); - sync_out.println("(c) 2024 Barak Shoshany (baraksh@gmail.com) (https://baraksh.com/)"); - sync_out.println("GitHub: https://github.com/bshoshany/thread-pool"); - sync_out.println(); + logln_ansi(ansi_title_italic, "BS::thread_pool:", " a fast, lightweight, modern, and easy-to-use C++17/C++20/C++23 thread pool library"); + logln_ansi(ansi_title_italic, "Copyright (c) 2021-2026 Barak Shoshany (baraksh@gmail.com) (https://baraksh.com/)"); + logln_ansi(ansi_title_italic, "GitHub: https://github.com/bshoshany/thread-pool"); + logln(); - sync_out.println("Thread pool library version is v", BS::thread_pool_version, '.'); - sync_out.println("Thread pool library imported using: ", BS::thread_pool_module ? "import BS.thread_pool (" : "#include \"BS_thread_pool.hpp\" (no ", "C++20 modules)."); - sync_out.println(); - sync_out.println("C++ Standard Library imported using:"); - sync_out.println("* Thread pool library: ", BS::thread_pool_import_std ? "import std (" : "#include <...> (no ", "C++23 std module)."); - sync_out.println("* Test program: ", using_import_std ? "import std (" : "#include <...> (no ", "C++23 std module)."); - sync_out.println(); - - sync_out.println("Native extensions are ", BS::thread_pool_native_extensions ? "enabled" : "disabled", '.'); - - sync_out.println(); - - sync_out.println("Detected OS: ", detect_os(), '.'); - sync_out.println("Detected compiler: ", detect_compiler(), '.'); - sync_out.println("Detected standard library: ", detect_lib(), '.'); - sync_out.println("Detected C++ standard: ", detect_cpp_standard(), '.'); - sync_out.println("Detected features:"); + print_key_values("Thread pool library version is: ", 'v', BS::thread_pool_version); + print_key_values("Thread pool library imported using: ", BS::thread_pool_module ? "import BS.thread_pool (" : "#include \"BS_thread_pool.hpp\" (no ", "C++20 modules)"); + logln(); + logln_ansi(ansi_title_underline, "C++ Standard Library imported using:"); + print_key_values("* Thread pool library: ", BS::thread_pool_import_std ? "import std (" : "#include <...> (no ", "C++23 std module)"); + print_key_values("* Test program: ", using_import_std ? "import std (" : "#include <...> (no ", "C++23 std module)"); + logln(); + print_key_values("Detected OS: ", detect_os()); + print_key_values("Detected compiler: ", detect_compiler()); + print_key_values("Detected standard library: ", detect_lib()); + print_key_values("Detected C++ standard: ", detect_cpp_standard(), " (__cplusplus = ", __cplusplus, ")"); + logln(); + logln_ansi(ansi_title_underline, "Detected features:"); print_features(); - - sync_out.println("Hardware concurrency is ", std::thread::hardware_concurrency(), '.'); - sync_out.println("Important: Please do not run any other applications, especially multithreaded applications, in parallel with this test!"); + print_key_values("Native extensions are: ", BS::thread_pool_native_extensions ? "Enabled" : "Disabled"); + print_key_values("Hardware concurrency is: ", std::thread::hardware_concurrency()); + logln(); + log_ansi(ansi_title_underline, "Important:"); + logln_ansi(ansi_title, " Please do not run any other applications, especially multithreaded applications, in parallel with this test!"); } /** @@ -3365,23 +3841,26 @@ std::string get_time() #if defined(_MSC_VER) && !defined(__cpp_lib_modules) // If MSVC is detected, use `localtime_s()` to avoid warning C4996. (This doesn't work if we used `import std`, so we check that to be on the safe side, although in that case `std::format` should be available anyway). if (localtime_s(&local_tm, &epoch) != 0) - return ""; + time_string = ""; #elif defined(__linux__) || defined(__APPLE__) // On Linux or macOS, use `localtime_r()` to avoid clang-tidy warning `concurrency-mt-unsafe`. if (localtime_r(&epoch, &local_tm) == nullptr) - return ""; + time_string = ""; #else - local_tm = *std::localtime(&epoch); + local_tm = *std::localtime(&epoch); // NOLINT(concurrency-mt-unsafe) #endif - const std::size_t bytes = std::strftime(time_string.data(), time_string.length() + 1, "%Y-%m-%d_%H.%M.%S", &local_tm); - if (bytes != time_string.length()) - return ""; + if (!time_string.empty()) + { + const std::size_t bytes = std::strftime(time_string.data(), time_string.length() + 1, "%Y-%m-%d_%H.%M.%S", &local_tm); + if (bytes != time_string.length()) + time_string = ""; + } return time_string; #endif } /** - * @brief A class to parse command line arguments. All arguments are assumed to be on/off and default to off. + * @brief A class to parse command line arguments. All arguments are simple on/off flags. */ class [[nodiscard]] arg_parser { @@ -3434,16 +3913,16 @@ public: int width = 1; for (const auto& [arg, opt] : allowed) width = std::max(width, static_cast(arg.size())); - sync_out.println("\nAvailable options (all are on/off and default to off):"); + logln("\nAvailable options (all are on/off and default to off):"); for (const auto& [arg, opt] : allowed) - sync_out.println(" ", std::left, std::setw(width), arg, " ", opt.desc); - sync_out.print("If no options are entered, the default is:\n "); + logln(" ", std::left, std::setw(width), arg, " ", opt.desc); + log("If no options are entered, the default is:\n "); for (const auto& [arg, opt] : allowed) { if (opt.def) - sync_out.print(arg, " "); + log(arg, " "); } - sync_out.println(); + logln(); } /** @@ -3492,6 +3971,7 @@ private: */ std::string_view executable; }; // class arg_parser +} // anonymous namespace int main(int argc, char* argv[]) // NOLINT(bugprone-exception-escape) { @@ -3499,9 +3979,14 @@ int main(int argc, char* argv[]) // NOLINT(bugprone-exception-escape) try { #endif - // If the file default_args.txt exists, read the default arguments from it (space separated in a single line). Otherwise, use the built-in defaults. This is useful when debugging. + // Disable ANSI colors if the environment variable `NO_COLOR` is set. + no_color = (std::getenv("NO_COLOR") != nullptr); // NOLINT(concurrency-mt-unsafe) + + // If the file default_args.txt exists in either this folder or the parent folder, read the default arguments from it (space separated in a single line). Otherwise, use the built-in defaults. This is useful when debugging. std::map defaults; std::ifstream default_args_file("default_args.txt"); + if (!default_args_file.is_open()) + default_args_file.open("../default_args.txt"); if (default_args_file.is_open()) { std::string line; @@ -3540,24 +4025,22 @@ int main(int argc, char* argv[]) // NOLINT(bugprone-exception-escape) { show_intro(); args.show_help(); - sync_out.println("\nERROR: No output stream specified! Please enter one or more of: log, stdout. Aborting."); + logln_ansi(ansi_error, "\nERROR: No output stream specified! Please enter one or more of: log, stdout. Aborting."); return 0; } if (!args["benchmarks"] && !args["deadlock"] && !args["plot"] && !args["tests"]) { show_intro(); args.show_help(); - sync_out.println("\nERROR: No tests or benchmarks requested! Please enter one or more of: benchmarks, deadlock, plot, tests. Aborting."); + logln_ansi(ansi_error, "\nERROR: No tests or benchmarks requested! Please enter one or more of: benchmarks, deadlock, plot, tests. Aborting."); return 0; } } - if (!args["stdout"]) - sync_out.remove_stream(std::cout); - // A stream object used to access the log file. std::ofstream log_file; + sync_log.remove_stream(std::cout); if (args["log"]) { // Extract the name of the executable file, or use a default value if it is not available. @@ -3571,16 +4054,19 @@ int main(int argc, char* argv[]) // NOLINT(bugprone-exception-escape) log_file.open(log_filename); if (log_file.is_open()) { - sync_out.print("Generating log file: ", log_filename, ".\n"); - sync_out.add_stream(log_file); + logln_ansi(ansi_info, "Generating log file: ", log_filename); + sync_log.add_stream(log_file); } else { - sync_out.println("ERROR: Could not create a log file."); + logln_ansi(ansi_error, "ERROR: Could not create a log file."); return 1; } } + use_stdout = args["stdout"]; + use_log = args["log"]; + show_intro(); if (args["tests"]) @@ -3615,7 +4101,7 @@ int main(int argc, char* argv[]) // NOLINT(bugprone-exception-escape) check_exceptions_submit(); check_exceptions_multi_future(); #else - print_header("NOTE: Exceptions are disabled, skipping wait deadlock check and exception handling tests."); + logln_ansi(ansi_info, "NOTE: Exceptions are disabled, skipping wait deadlock check and exception handling tests."); #endif print_header("Checking detach_loop() and submit_loop():"); @@ -3664,42 +4150,42 @@ int main(int argc, char* argv[]) // NOLINT(bugprone-exception-escape) #ifndef _WIN32 if ((args["benchmarks"] || args["plot"]) && !BS::set_os_process_priority(BS::os_process_priority::realtime)) { - sync_out.println("NOTE: Skipping process/thread priority checks since the test is running on Linux/macOS without root privileges and benchmarks are enabled. On Linux/macOS, if priorities are decreased, they cannot be increased back to normal without root privileges, so the process will be stuck on the lowest priority, and the benchmarks will be unreliable.\n"); + logln_ansi(ansi_info, "NOTE: Skipping process/thread priority checks since the test is running on Linux/macOS without root privileges and benchmarks are enabled. On Linux/macOS, if priorities are decreased, they cannot be increased back to normal without root privileges, so the process will be stuck on the lowest priority, and the benchmarks will be unreliable.\n"); } else #endif { // Note: We have to check thread priorities first, because the check for process priorities lowers the priority of the process to the lowest level, and on Linux, if not running as root, we can only lower the priority, not raise it, so the process gets stuck on the lowest priority. Since the thread priorities cannot be set to higher than the process priorities, this means the thread priorities will also be stuck on the lowest priority, and the test will fail. check_os_thread_priorities(); - sync_out.println(); + logln(); check_os_process_priorities(); - sync_out.println(); + logln(); } check_os_thread_names(); - sync_out.println(); + logln(); #if defined(_WIN32) || defined(__linux__) check_os_thread_affinity(); - sync_out.println(); + logln(); check_os_process_affinity(); #else - sync_out.println("NOTE: macOS does not support affinity, skipping the corresponding test."); + logln_ansi(ansi_info, "NOTE: macOS does not support affinity, skipping the corresponding test."); #endif #else - print_header("NOTE: Native extensions disabled, skipping the corresponding test."); + logln_ansi(ansi_info, "NOTE: Native extensions disabled, skipping the corresponding test."); #endif } if (args["deadlock"]) { print_header("Checking for deadlocks:"); - sync_out.println("Checking for destruction deadlocks..."); + logln("Checking for destruction deadlocks..."); check_deadlock( [] { BS::thread_pool temp_pool; temp_pool.detach_task([] {}); }); - sync_out.println("Checking for reset deadlocks..."); + logln("Checking for reset deadlocks..."); BS::thread_pool temp_pool; check_deadlock( [&temp_pool] @@ -3710,14 +4196,14 @@ int main(int argc, char* argv[]) // NOLINT(bugprone-exception-escape) if (test_results::tests_failed > 0) { - print_header("FAILURE: Passed " + std::to_string(test_results::tests_succeeded) + " checks, but failed " + std::to_string(test_results::tests_failed) + "!", '+'); - sync_out.println("\nPlease submit a bug report at https://github.com/bshoshany/thread-pool/issues including the exact specifications of your system (OS, CPU, compiler, etc.) and the generated log file."); + print_header("FAILURE: Passed " + std::to_string(test_results::tests_succeeded) + " checks, but failed " + std::to_string(test_results::tests_failed) + "!", '+', ansi_error); + logln_ansi(ansi_error, "\nPlease submit a bug report at https://github.com/bshoshany/thread-pool/issues including the exact specifications of your system (OS, CPU, compiler, etc.) and the generated log file."); log_file.close(); return static_cast(test_results::tests_failed); } - if (args["tests"]) - print_header("SUCCESS: Passed all " + std::to_string(test_results::tests_succeeded) + " checks!", '+'); + if (test_results::tests_succeeded > 0) + print_header("SUCCESS: Passed all " + std::to_string(test_results::tests_succeeded) + " checks!", '+', ansi_success); if (args["benchmarks"] || args["plot"]) check_performance(args["benchmarks"], args["plot"], args["save"]); @@ -3728,7 +4214,7 @@ int main(int argc, char* argv[]) // NOLINT(bugprone-exception-escape) } catch (const std::exception& e) { - sync_out.println("ERROR: Tests failed due to exception: ", e.what()); + logln_ansi(ansi_error, "ERROR: Tests failed due to exception: ", e.what()); return 1; } #endif