From dd399d6b754c3a7fa0a105b7e78646fdc674f2d5 Mon Sep 17 00:00:00 2001 From: Bensuperpc Date: Thu, 25 Nov 2021 16:03:58 +0100 Subject: [PATCH 1/2] Big update (See commit details) Add github action for Linux, MacOS, Windows Add github action for cross platform (ARM, RISCV, PowerPC ect...) with dockcross Add Google test and Google benchmark, to avoid regressions with units test Add CMake (Shared or Static ThreadPool library), can be installed (Tested only on ArchLinux/Manjaro and ubuntu 21.10) Add optional CMake Presets (CMake 3.19 min) Add Examples Improve Readme CMake library based on cmake-init Improve warning message with MSCV Update readme README.md Signed-off-by: Bensuperpc --- .clang-format | 178 +++++++++++++++ .clang-tidy | 153 +++++++++++++ .codespellrc | 6 + .github/workflows/base.yml | 169 ++++++++++++++ .github/workflows/dockcross.yml | 205 +++++++++++++++++ .github/workflows/linux.yml | 53 +++++ .github/workflows/macos.yml | 46 ++++ .github/workflows/windows.yml | 45 ++++ .gitignore | 9 + BUILDING.md | 47 ++++ CMakeLists.txt | 62 ++++++ CMakePresets.json | 133 +++++++++++ CODE_OF_CONDUCT.md | 5 + CONTRIBUTING.md | 19 ++ HACKING.md | 127 +++++++++++ README.md | 20 +- ThreadPool.h | 306 -------------------------- cmake/coverage.cmake | 33 +++ cmake/dev-mode.cmake | 25 +++ cmake/docs.cmake | 50 +++++ cmake/folders.cmake | 21 ++ cmake/install-config.cmake | 1 + cmake/install-rules.cmake | 61 +++++ cmake/lint-targets.cmake | 34 +++ cmake/lint.cmake | 52 +++++ cmake/open-cpp-coverage.cmake.example | 30 +++ cmake/prelude.cmake | 17 ++ cmake/project-is-top-level.cmake | 6 + cmake/spell-targets.cmake | 22 ++ cmake/spell.cmake | 29 +++ cmake/variables.cmake | 28 +++ cmake/windows-set-path.cmake | 22 ++ docs/Doxyfile.in | 32 +++ docs/conf.py.in | 6 + docs/pages/about.dox | 7 + example.cpp | 34 --- example/CMakeLists.txt | 31 +++ example/example_1.cpp | 15 ++ example/example_2.cpp | 33 +++ example/function_example.cpp | 31 +++ include/ThreadPool/ThreadPool.hpp | 284 ++++++++++++++++++++++++ test/CMakeLists.txt | 90 ++++++++ test/source/ThreadPool_bench.cpp | 37 ++++ test/source/ThreadPool_test.cpp | 54 +++++ tools/dockcross-cmake-builder.sh | 30 +++ tools/graphic.py | 64 ++++++ tools/ninja-builder.sh | 12 + 47 files changed, 2422 insertions(+), 352 deletions(-) create mode 100644 .clang-format create mode 100644 .clang-tidy create mode 100644 .codespellrc create mode 100644 .github/workflows/base.yml create mode 100644 .github/workflows/dockcross.yml create mode 100644 .github/workflows/linux.yml create mode 100644 .github/workflows/macos.yml create mode 100644 .github/workflows/windows.yml create mode 100644 .gitignore create mode 100644 BUILDING.md create mode 100644 CMakeLists.txt create mode 100644 CMakePresets.json create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 HACKING.md delete mode 100755 ThreadPool.h create mode 100644 cmake/coverage.cmake create mode 100644 cmake/dev-mode.cmake create mode 100644 cmake/docs.cmake create mode 100644 cmake/folders.cmake create mode 100644 cmake/install-config.cmake create mode 100644 cmake/install-rules.cmake create mode 100644 cmake/lint-targets.cmake create mode 100644 cmake/lint.cmake create mode 100644 cmake/open-cpp-coverage.cmake.example create mode 100644 cmake/prelude.cmake create mode 100644 cmake/project-is-top-level.cmake create mode 100644 cmake/spell-targets.cmake create mode 100644 cmake/spell.cmake create mode 100644 cmake/variables.cmake create mode 100644 cmake/windows-set-path.cmake create mode 100644 docs/Doxyfile.in create mode 100644 docs/conf.py.in create mode 100644 docs/pages/about.dox delete mode 100644 example.cpp create mode 100644 example/CMakeLists.txt create mode 100644 example/example_1.cpp create mode 100644 example/example_2.cpp create mode 100644 example/function_example.cpp create mode 100644 include/ThreadPool/ThreadPool.hpp create mode 100644 test/CMakeLists.txt create mode 100644 test/source/ThreadPool_bench.cpp create mode 100644 test/source/ThreadPool_test.cpp create mode 100644 tools/dockcross-cmake-builder.sh create mode 100644 tools/graphic.py create mode 100644 tools/ninja-builder.sh diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..3f4f773 --- /dev/null +++ b/.clang-format @@ -0,0 +1,178 @@ +--- +Language: Cpp +# BasedOnStyle: Chromium +AccessModifierOffset: -2 +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: false +AlignConsecutiveAssignments: false +AlignConsecutiveBitFields: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: DontAlign +AlignOperands: DontAlign +AlignTrailingComments: false +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortEnumsOnASingleLine: false +AllowShortBlocksOnASingleLine: Empty +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline +AllowShortLambdasOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: MultiLine + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: false + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: true + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeBraces: Custom +# BreakBeforeInheritanceComma: true +BreakInheritanceList: BeforeComma +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: true +BreakConstructorInitializers: BeforeComma +BreakAfterJavaFieldAnnotations: true +BreakStringLiterals: true +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DeriveLineEnding: false +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Regroup +IncludeCategories: + # Standard library headers come before anything else + - Regex: '^<[a-z_]+>' + Priority: -1 + - Regex: '^<.+\.h(pp)?>' + Priority: 1 + - Regex: '^<.*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IncludeIsMainRegex: '' +IncludeIsMainSourceRegex: '' +IndentCaseLabels: true +IndentCaseBlocks: false +IndentGotoLabels: true +IndentPPDirectives: AfterHash +IndentExternBlock: NoIndent +IndentWidth: 2 +IndentWrappedFunctionNames: false +InsertTrailingCommas: Wrapped +JavaScriptQuotes: Double +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Never +ObjCBlockIndentWidth: 2 +ObjCBreakBeforeNestedBlockParam: true +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +RawStringFormats: + - Language: Cpp + Delimiters: + - cc + - CC + - cpp + - Cpp + - CPP + - 'c++' + - 'C++' + CanonicalDelimiter: '' + BasedOnStyle: google + - Language: TextProto + Delimiters: + - pb + - PB + - proto + - PROTO + EnclosingFunctions: + - EqualsProto + - EquivToProto + - PARSE_PARTIAL_TEXT_PROTO + - PARSE_TEST_PROTO + - PARSE_TEXT_PROTO + - ParseTextOrDie + - ParseTextProtoOrDie + - ParseTestProto + - ParsePartialTestProto + CanonicalDelimiter: '' + BasedOnStyle: google +ReflowComments: true +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: true +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatementsExceptForEachMacros +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: false +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpaceBeforeSquareBrackets: false +Standard: Auto +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 8 +UseCRLF: false +UseTab: Never +WhitespaceSensitiveMacros: + - STRINGIZE + - PP_STRINGIZE + - BOOST_PP_STRINGIZE +... diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..f932a80 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,153 @@ +--- +# Enable ALL the things! Except not really +# misc-non-private-member-variables-in-classes: the options don't do anything +Checks: "*,\ + -google-readability-todo,\ + -altera-unroll-loops,\ + -fuchsia-*,\ + fuchsia-multiple-inheritance,\ + -llvm-header-guard,\ + -llvm-include-order,\ + -llvmlibc-*,\ + -misc-non-private-member-variables-in-classes" +WarningsAsErrors: '' +CheckOptions: + - key: 'bugprone-argument-comment.StrictMode' + value: 'true' +# Prefer using enum classes with 2 values for parameters instead of bools + - key: 'bugprone-argument-comment.CommentBoolLiterals' + value: 'true' + - key: 'bugprone-misplaced-widening-cast.CheckImplicitCasts' + value: 'true' + - key: 'bugprone-sizeof-expression.WarnOnSizeOfIntegerExpression' + value: 'true' + - key: 'bugprone-suspicious-string-compare.WarnOnLogicalNotComparison' + value: 'true' + - key: 'readability-simplify-boolean-expr.ChainedConditionalReturn' + value: 'true' + - key: 'readability-simplify-boolean-expr.ChainedConditionalAssignment' + value: 'true' + - key: 'readability-uniqueptr-delete-release.PreferResetCall' + value: 'true' + - key: 'cppcoreguidelines-init-variables.MathHeader' + value: '' + - key: 'cppcoreguidelines-narrowing-conversions.PedanticMode' + value: 'true' + - key: 'readability-else-after-return.WarnOnUnfixable' + value: 'true' + - key: 'readability-else-after-return.WarnOnConditionVariables' + value: 'true' + - key: 'readability-inconsistent-declaration-parameter-name.Strict' + value: 'true' + - key: 'readability-qualified-auto.AddConstToQualified' + value: 'true' + - key: 'readability-redundant-access-specifiers.CheckFirstDeclaration' + value: 'true' +# These seem to be the most common identifier styles + - key: 'readability-identifier-naming.AbstractClassCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ClassCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ClassConstantCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ClassMemberCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ClassMethodCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ConstantCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ConstantMemberCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ConstantParameterCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ConstantPointerParameterCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ConstexprFunctionCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ConstexprMethodCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ConstexprVariableCase' + value: 'lower_case' + - key: 'readability-identifier-naming.EnumCase' + value: 'lower_case' + - key: 'readability-identifier-naming.EnumConstantCase' + value: 'lower_case' + - key: 'readability-identifier-naming.FunctionCase' + value: 'lower_case' + - key: 'readability-identifier-naming.GlobalConstantCase' + value: 'lower_case' + - key: 'readability-identifier-naming.GlobalConstantPointerCase' + value: 'lower_case' + - key: 'readability-identifier-naming.GlobalFunctionCase' + value: 'lower_case' + - key: 'readability-identifier-naming.GlobalPointerCase' + value: 'lower_case' + - key: 'readability-identifier-naming.GlobalVariableCase' + value: 'lower_case' + - key: 'readability-identifier-naming.InlineNamespaceCase' + value: 'lower_case' + - key: 'readability-identifier-naming.LocalConstantCase' + value: 'lower_case' + - key: 'readability-identifier-naming.LocalConstantPointerCase' + value: 'lower_case' + - key: 'readability-identifier-naming.LocalPointerCase' + value: 'lower_case' + - key: 'readability-identifier-naming.LocalVariableCase' + value: 'lower_case' + - key: 'readability-identifier-naming.MacroDefinitionCase' + value: 'UPPER_CASE' + - key: 'readability-identifier-naming.MemberCase' + value: 'lower_case' + - key: 'readability-identifier-naming.MethodCase' + value: 'lower_case' + - key: 'readability-identifier-naming.NamespaceCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ParameterCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ParameterPackCase' + value: 'lower_case' + - key: 'readability-identifier-naming.PointerParameterCase' + value: 'lower_case' + - key: 'readability-identifier-naming.PrivateMemberCase' + value: 'lower_case' + - key: 'readability-identifier-naming.PrivateMemberPrefix' + value: 'm_' + - key: 'readability-identifier-naming.PrivateMethodCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ProtectedMemberCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ProtectedMemberPrefix' + value: 'm_' + - key: 'readability-identifier-naming.ProtectedMethodCase' + value: 'lower_case' + - key: 'readability-identifier-naming.PublicMemberCase' + value: 'lower_case' + - key: 'readability-identifier-naming.PublicMethodCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ScopedEnumConstantCase' + value: 'lower_case' + - key: 'readability-identifier-naming.StaticConstantCase' + value: 'lower_case' + - key: 'readability-identifier-naming.StaticVariableCase' + value: 'lower_case' + - key: 'readability-identifier-naming.StructCase' + value: 'lower_case' + - key: 'readability-identifier-naming.TemplateParameterCase' + value: 'CamelCase' + - key: 'readability-identifier-naming.TemplateTemplateParameterCase' + value: 'CamelCase' + - key: 'readability-identifier-naming.TypeAliasCase' + value: 'lower_case' + - key: 'readability-identifier-naming.TypedefCase' + value: 'lower_case' + - key: 'readability-identifier-naming.TypeTemplateParameterCase' + value: 'CamelCase' + - key: 'readability-identifier-naming.UnionCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ValueTemplateParameterCase' + value: 'CamelCase' + - key: 'readability-identifier-naming.VariableCase' + value: 'lower_case' + - key: 'readability-identifier-naming.VirtualMethodCase' + value: 'lower_case' +... diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 0000000..1e8883e --- /dev/null +++ b/.codespellrc @@ -0,0 +1,6 @@ +[codespell] +builtin = clear,rare,en-GB_to_en-US,names,informal,code +check-filenames = +check-hidden = +skip = */.git,*/build +quiet-level = 2 diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml new file mode 100644 index 0000000..b793eda --- /dev/null +++ b/.github/workflows/base.yml @@ -0,0 +1,169 @@ +name: Continuous Integration + +on: + push: + branches: + - master + + pull_request: + branches: + - master + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-python@v2 + with: { python-version: "3.8" } + + - name: Install codespell + run: pip3 install codespell + + - name: Lint + run: cmake -D FORMAT_COMMAND=clang-format -P cmake/lint.cmake + + - name: Spell check + if: always() + run: cmake -P cmake/spell.cmake + + coverage: + needs: [lint] + + runs-on: ubuntu-latest + + # To enable coverage, go to https://codecov.io/, acquire a token, put it + # into your secrets (Settings > Secrets > New repository secret), delete + # the last line from the conditional below and edit the "" + # placeholder to your GitHub name. + # If you do not wish to use codecov, then simply delete this job from the + # workflow. + if: github.repository_owner == '' + && false + + steps: + - uses: actions/checkout@v2 + + - name: Install LCov + run: sudo apt-get update -q + && sudo apt-get install lcov -q -y + + - name: Configure + run: cmake --preset=ci-coverage + + - name: Build + run: cmake --build build/coverage -j 2 + + - name: Test + working-directory: build/coverage + run: ctest --output-on-failure -j 2 + + - name: Process coverage info + run: cmake --build build/coverage -t coverage + + - name: Submit to codecov.io + uses: codecov/codecov-action@v1 + with: + file: build/coverage/coverage.info + + sanitize: + needs: [lint] + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Configure + env: { CXX: clang++-11 } + run: cmake --preset=ci-sanitize + + - name: Build + run: cmake --build build/sanitize -j 2 + + - name: Test + working-directory: build/sanitize + env: + ASAN_OPTIONS: "strict_string_checks=1:\ + detect_stack_use_after_return=1:\ + check_initialization_order=1:\ + strict_init_order=1:\ + detect_leaks=1" + UBSAN_OPTIONS: print_stacktrace=1 + run: ctest --output-on-failure -j 2 + + test: + needs: [lint] + + strategy: + matrix: + os: [macos, ubuntu, windows] + + runs-on: ${{ matrix.os }}-latest + + steps: + - uses: actions/checkout@v2 + + - name: Install static analyzers + if: matrix.os == 'ubuntu' + run: sudo apt-get install clang-tidy cppcheck -y -q + + - name: Configure + run: cmake --preset=ci-${{ matrix.os }} + + - name: Build + run: cmake --build build --config Release -j 2 + + - name: Install + run: cmake --install build --config Release --prefix prefix + + - name: Test + working-directory: build + run: ctest --output-on-failure -C Release -j 2 + + docs: + # Deploy docs only when builds succeed + needs: [sanitize, test] + + runs-on: ubuntu-latest + + # To enable, first you have to create an orphaned gh-pages branch: + # + # git switch --orphan gh-pages + # git commit --allow-empty -m "Initial commit" + # git push -u origin gh-pages + # + # Edit the placeholder below to your GitHub name, so this action + # runs only in your repository and no one else's fork. After these, delete + # this comment and the last line in the conditional below. + # If you do not wish to use GitHub Pages for deploying documentation, then + # simply delete this job similarly to the coverage one. + if: github.ref == 'refs/heads/master' + && github.event_name == 'push' + && github.repository_owner == '' + && false + + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-python@v2 + with: { python-version: "3.8" } + + - name: Install m.css dependencies + run: pip3 install jinja2 Pygments + + - name: Install Doxygen + run: sudo apt-get update -q + && sudo apt-get install doxygen -q -y + + - name: Build docs + run: cmake -B build -D "CMAKE_PROJECT_INCLUDE=$PWD/cmake/docs.cmake" + && cmake --build build --target docs + + - name: Deploy docs + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: build/docs/html diff --git a/.github/workflows/dockcross.yml b/.github/workflows/dockcross.yml new file mode 100644 index 0000000..c38ed0c --- /dev/null +++ b/.github/workflows/dockcross.yml @@ -0,0 +1,205 @@ +name: Dockcross CI + +on: + push: + branches: + - "*" + paths-ignore: + - "**.md" + pull_request: + branches: + - "*" + workflow_dispatch: + +jobs: + build: + name: dockcross ${{ matrix.dockcross.image_name }} ${{ matrix.build_type }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + dockcross: + # Android images + - { image_name: android-arm, cmake_arg: "-DCMAKE_CXX_STANDARD=17" } + - { image_name: android-arm64, cmake_arg: "-DCMAKE_CXX_STANDARD=17" } + - { image_name: android-x86, cmake_arg: "-DCMAKE_CXX_STANDARD=17" } + - { image_name: android-x86_64, cmake_arg: "-DCMAKE_CXX_STANDARD=17" } + + # Web images + - { image_name: web-wasm, cmake_arg: "-DCMAKE_CXX_STANDARD=17" } + - { + image_name: web-wasi, + cmake_arg: "-DCMAKE_CXX_FLAGS='${CMAKE_CXX_FLAGS} -fno-exceptions'", + } + + # Linux ARMv5 images + - { image_name: linux-armv5, cmake_arg: "-DCMAKE_CXX_STANDARD=17" } + - { + image_name: linux-armv5-musl, + cmake_arg: "-DCMAKE_CXX_STANDARD=17", + } + + # Linux ARMv6 images + - { image_name: linux-armv6, cmake_arg: "-DCMAKE_CXX_STANDARD=17" } + - { + image_name: linux-armv6-lts, + cmake_arg: "-DCMAKE_CXX_STANDARD=17", + } + - { + image_name: linux-armv6-musl, + cmake_arg: "-DCMAKE_CXX_STANDARD=17", + } + + # linux ARMv7 images + - { image_name: linux-armv7, cmake_arg: "-DCMAKE_CXX_STANDARD=17" } + - { + image_name: linux-armv7-lts, + cmake_arg: "-DCMAKE_CXX_STANDARD=17", + } + - { image_name: linux-armv7a, cmake_arg: "-DCMAKE_CXX_STANDARD=17" } + - { + image_name: linux-armv7l-musl, + cmake_arg: "-DCMAKE_CXX_STANDARD=17", + } + + # Linux ARMv8 (64 bit) images + - { image_name: linux-arm64, cmake_arg: "-DCMAKE_CXX_STANDARD=17" } + - { + image_name: linux-arm64-lts, + cmake_arg: "-DCMAKE_CXX_STANDARD=17", + } + - { + image_name: linux-arm64-full, + cmake_arg: "-DCMAKE_CXX_STANDARD=17", + } + - { + image_name: linux-arm64-musl, + cmake_arg: "-DCMAKE_CXX_STANDARD=17", + } + + # Linux x86 images + - { image_name: linux-x86, cmake_arg: "-DCMAKE_CXX_STANDARD=17" } + - { image_name: linux-x64, cmake_arg: "-DCMAKE_CXX_STANDARD=17" } + - { + image_name: linux-x64-clang, + cmake_arg: "-DCMAKE_CXX_STANDARD=17", + } + - { + image_name: linux-x64-tinycc, + cmake_arg: "-DCMAKE_CXX_STANDARD=17", + } + - { + image_name: linux-x86_64-full, + cmake_arg: "-DCMAKE_CXX_STANDARD=17", + } + + # Linux s390 images + - { image_name: linux-s390x, cmake_arg: "-DCMAKE_CXX_STANDARD=17" } + + # Linux mips images + - { image_name: linux-mips, cmake_arg: "-DCMAKE_CXX_STANDARD=17" } + + # Linux PowerPC 64 images + - { image_name: linux-ppc64le, cmake_arg: "-DCMAKE_CXX_STANDARD=17" } + + # Linux xtensa images + - { + image_name: linux-xtensa-uclibc, + cmake_arg: "-DCMAKE_CXX_STANDARD=17", + } + + # Linux riscv images + - { image_name: linux-riscv32, cmake_arg: "-DCMAKE_CXX_STANDARD=17" } + - { image_name: linux-riscv64, cmake_arg: "-DCMAKE_CXX_STANDARD=17" } + + # Linux m68k images + - { + image_name: linux-m68k-uclibc, + cmake_arg: "-DCMAKE_CXX_STANDARD=17", + } + + # ManyLinux images + - { + image_name: manylinux1-x64, + cmake_arg: "-DCMAKE_CXX_STANDARD=14 -D CMAKE_C_STANDARD=99", + } + - { + image_name: manylinux1-x86, + cmake_arg: "-DCMAKE_CXX_STANDARD=14 -D CMAKE_C_STANDARD=99", + } + - { + image_name: manylinux2010-x86, + cmake_arg: "-DCMAKE_CXX_STANDARD=17", + } + - { + image_name: manylinux2010-x64, + cmake_arg: "-DCMAKE_CXX_STANDARD=17", + } + - { + image_name: manylinux2014-x86, + cmake_arg: "-DCMAKE_CXX_STANDARD=17", + } + - { + image_name: manylinux2014-x64, + cmake_arg: "-DCMAKE_CXX_STANDARD=17", + } + # - { image_name: manylinux2014-aarch64, cmake_arg: "-DCMAKE_CXX_STANDARD=17" } + + # Windows x86 images + - { + image_name: windows-static-x64, + cmake_arg: "-DCMAKE_CXX_STANDARD=17", + } + - { + image_name: windows-static-x64-posix, + cmake_arg: "-DCMAKE_CXX_STANDARD=17", + } + - { + image_name: windows-static-x86, + cmake_arg: "-DCMAKE_CXX_STANDARD=17", + } + - { + image_name: windows-shared-x64, + cmake_arg: "-DCMAKE_CXX_STANDARD=17", + } + - { + image_name: windows-shared-x64-posix, + cmake_arg: "-DCMAKE_CXX_STANDARD=17", + } + - { + image_name: windows-shared-x86, + cmake_arg: "-DCMAKE_CXX_STANDARD=17", + } + + # Windows ARM images + - { + image_name: windows-armv7, + cmake_arg: "-DCMAKE_BUILD_WITH_INSTALL_RPATH=ON -DCMAKE_CXX_STANDARD=17", + } + - { + image_name: windows-arm64, + cmake_arg: "-DCMAKE_BUILD_WITH_INSTALL_RPATH=ON -DCMAKE_CXX_STANDARD=17", + } + + # Disable MinSizeRel RelWithDebInfo, Release, Debug + build_type: [Release] + steps: + - name: "Checkout Code" + uses: actions/checkout@v2 + with: + submodules: "recursive" + fetch-depth: 0 + + - name: "Pull docker image: dockcross/${{ matrix.dockcross.image_name }}" + run: docker pull dockcross/${{ matrix.dockcross.image_name }}:latest + + - name: "Make dockcross script: dockcross-${{ matrix.dockcross.image_name }}" + run: | + docker run --rm dockcross/${{ matrix.dockcross.image_name }} > ./dockcross-${{ matrix.dockcross.image_name }} + chmod +x ./dockcross-${{ matrix.dockcross.image_name }} + + - name: "Config CMake build" + run: ./dockcross-${{ matrix.dockcross.image_name }} cmake -B build-${{ matrix.dockcross.image_name }} -S . -G Ninja -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} ${{ matrix.dockcross.cmake_arg }} + + - name: "Build" + run: ./dockcross-${{ matrix.dockcross.image_name }} ninja -C build-${{ matrix.dockcross.image_name }} diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml new file mode 100644 index 0000000..3a5005d --- /dev/null +++ b/.github/workflows/linux.yml @@ -0,0 +1,53 @@ +name: linux + +on: + push: + branches: + - "*" + paths-ignore: + - "**.md" + pull_request: + branches: + - "*" + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + cxx_compiler: [g++, clang++] + cc_compiler: [gcc] + shared: [YES, NO] + standard: [11, 14, 17, 20] + build_type: [Debug, Release] + + steps: + - name: "Checkout Code" + uses: actions/checkout@v2 + with: + submodules: "recursive" + fetch-depth: 0 + + - name: dependencies + run: | + sudo apt -y update + sudo apt -y install ninja-build + + - name: Configure + env: + CXX: ${{matrix.cxx_compiler}} + CC: ${{matrix.cc_compiler}} + run: | + cmake -S . -B build -D CMAKE_BUILD_TYPE=${{matrix.build_type}} \ + -D CMAKE_CXX_STANDARD=${{matrix.standard}} -G Ninja \ + -D BUILD_SHARED_LIBS=${{matrix.shared}} --preset=ci-coverage + + - name: Build + run: ninja -C build + + - name: Test + run: ctest --test-dir build --verbose --output-on-failure --parallel 2 + env: + CTEST_OUTPUT_ON_FAILURE: True diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml new file mode 100644 index 0000000..4971284 --- /dev/null +++ b/.github/workflows/macos.yml @@ -0,0 +1,46 @@ +name: macos + +on: + push: + branches: + - "*" + paths-ignore: + - "**.md" + pull_request: + branches: + - "*" + workflow_dispatch: + +jobs: + build: + runs-on: macos-latest + strategy: + fail-fast: false + matrix: + build_type: [Debug, Release] + shared: [YES, NO] + standard: [11, 14, 17, 20] + + steps: + - name: "Checkout Code" + uses: actions/checkout@v2 + with: + submodules: "recursive" + fetch-depth: 0 + + - name: Create Build Environment + run: cmake -E make_directory ${{runner.workspace}}/build + + - name: Configure + run: | + cmake -S . -B build -DCMAKE_BUILD_TYPE=${{matrix.build_type}} \ + -D BUILD_SHARED_LIBS=${{matrix.shared}} \ + -D CMAKE_CXX_STANDARD=${{matrix.standard}} --preset=ci-macos + + - name: Build + run: cmake --build build --config ${{matrix.build_type}} --parallel 2 + + - name: Test + run: ctest --test-dir build --verbose --output-on-failure --parallel 2 + env: + CTEST_OUTPUT_ON_FAILURE: True diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml new file mode 100644 index 0000000..1dd9d40 --- /dev/null +++ b/.github/workflows/windows.yml @@ -0,0 +1,45 @@ +name: windows + +on: + push: + branches: + - "*" + paths-ignore: + - "**.md" + pull_request: + branches: + - "*" + workflow_dispatch: + +jobs: + build: + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + shared: [YES, NO] + standard: [11, 14, 17, 20] + platform: [Win32, x64] + build_type: [Debug, Release] + steps: + - name: "Checkout Code" + uses: actions/checkout@v2 + with: + submodules: "recursive" + fetch-depth: 0 + + - name: Create Build Environment + run: cmake -E make_directory ${{runner.workspace}}/build + + - name: Configure + run: cmake -S . -B build -D CMAKE_BUILD_TYPE=${{matrix.build_type}} + -A ${{matrix.platform}} -D CMAKE_CXX_STANDARD=${{matrix.standard}} + -D BUILD_SHARED_LIBS=${{matrix.shared}} --preset=ci-windows + + - name: Build + run: cmake --build build --parallel 2 + + - name: Test + run: ctest --test-dir build --verbose --output-on-failure --parallel 2 + env: + CTEST_OUTPUT_ON_FAILURE: True diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..75622aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.idea/ +.vs/ +.vscode/ +build/ +cmake/open-cpp-coverage.cmake +cmake-build-*/ +prefix/ +CMakeLists.txt.user +CMakeUserPresets.json diff --git a/BUILDING.md b/BUILDING.md new file mode 100644 index 0000000..088fb8c --- /dev/null +++ b/BUILDING.md @@ -0,0 +1,47 @@ +# Building with CMake + +## Build + +This project doesn't require any special command-line flags to build to keep +things simple. + +Here are the steps for building in release mode with a single-configuration +generator, like the Unix Makefiles one: + +```sh +cmake -S . -B build -D CMAKE_BUILD_TYPE=Release +cmake --build build +``` + +Here are the steps for building in release mode with a multi-configuration +generator, like the Visual Studio ones: + +```sh +cmake -S . -B build +cmake --build build --config Release +``` + +## Install + +This project doesn't require any special command-line flags to install to keep +things simple. As a prerequisite, the project has to be built with the above +commands already. + +The below commands require at least CMake 3.15 to run, because that is the +version in which [Install a Project][1] was added. + +Here is the command for installing the release mode artifacts with a +single-configuration generator, like the Unix Makefiles one: + +```sh +cmake --install build +``` + +Here is the command for installing the release mode artifacts with a +multi-configuration generator, like the Visual Studio ones: + +```sh +cmake --install build --config Release +``` + +[1]: https://cmake.org/cmake/help/latest/manual/cmake.1.html#install-a-project diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7a74f90 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,62 @@ +cmake_minimum_required(VERSION 3.14) + +include(cmake/prelude.cmake) + +project( + ThreadPool + VERSION 0.1.0 + DESCRIPTION "A simple C++11 Thread Pool implementation" + HOMEPAGE_URL "https://github.com/log4cplus/ThreadPool" + LANGUAGES NONE +) + +docs_early_return() + +include(cmake/project-is-top-level.cmake) +include(cmake/variables.cmake) + +# ---- Declare library ---- + +add_library(ThreadPool_ThreadPool INTERFACE) +add_library(ThreadPool::ThreadPool ALIAS ThreadPool_ThreadPool) + +set_property( + TARGET ThreadPool_ThreadPool PROPERTY + EXPORT_NAME ThreadPool +) + +target_include_directories( + ThreadPool_ThreadPool ${warning_guard} + INTERFACE + "$" +) + +#target_compile_features(ThreadPool_ThreadPool INTERFACE cxx_std_17) + +# ---- Install rules ---- + +if(NOT CMAKE_SKIP_INSTALL_RULES) + include(cmake/install-rules.cmake) +endif() + +# ---- Examples ---- + +if(PROJECT_IS_TOP_LEVEL) + option(BUILD_EXAMPLES "Build examples tree." "${ThreadPool_DEVELOPER_MODE}") + if(BUILD_EXAMPLES) + add_subdirectory(example) + endif() +endif() + +# ---- Developer mode ---- + +if(NOT ThreadPool_DEVELOPER_MODE) + return() +elseif(NOT PROJECT_IS_TOP_LEVEL) + message( + AUTHOR_WARNING + "Developer mode is intended for developers of ThreadPool" + ) +endif() + +include(cmake/dev-mode.cmake) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..dd1dc87 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,133 @@ +{ + "version": 1, + "cmakeMinimumRequired": { + "major": 3, + "minor": 14, + "patch": 0 + }, + "configurePresets": [ + { + "name": "cmake-pedantic", + "hidden": true, + "warnings": { + "dev": true, + "deprecated": true, + "uninitialized": true, + "unusedCli": true, + "systemVars": false + }, + "errors": { + "dev": false, + "deprecated": false + } + }, + { + "name": "dev-mode", + "hidden": true, + "inherits": "cmake-pedantic", + "cacheVariables": { + "ThreadPool_DEVELOPER_MODE": "ON" + } + }, + { + "name": "cppcheck", + "hidden": true, + "cacheVariables": { + "CMAKE_CXX_CPPCHECK": "cppcheck;--inline-suppr" + } + }, + { + "name": "clang-tidy", + "hidden": true, + "cacheVariables": { + "CMAKE_CXX_CLANG_TIDY": "clang-tidy;--header-filter=${sourceDir}/*" + } + }, + { + "name": "ci-std", + "description": "This preset makes sure the project actually builds with at least the specified standard", + "hidden": true, + "cacheVariables": { + "CMAKE_CXX_EXTENSIONS": "OFF", + "CMAKE_CXX_STANDARD": "17", + "CMAKE_CXX_STANDARD_REQUIRED": "ON" + } + }, + { + "name": "flags-unix", + "hidden": true, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "-Wall -Wextra -Wpedantic -Wshadow -Wold-style-cast" + } + }, + { + "name": "flags-windows", + "hidden": true, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/W4 /permissive- /utf-8 /volatile:iso /EHsc /Zc:__cplusplus /Zc:throwingNew" + } + }, + { + "name": "ci-unix", + "generator": "Unix Makefiles", + "hidden": true, + "inherits": ["flags-unix", "ci-std"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "ci-win64", + "inherits": ["flags-windows", "ci-std"], + "generator": "Visual Studio 16 2019", + "architecture": "x64", + "hidden": true + }, + { + "name": "coverage-unix", + "binaryDir": "${sourceDir}/build/coverage", + "inherits": "ci-unix", + "hidden": true, + "cacheVariables": { + "ENABLE_COVERAGE": "ON", + "CMAKE_BUILD_TYPE": "Coverage", + "CMAKE_CXX_FLAGS_COVERAGE": "-Og -g --coverage -fkeep-inline-functions -fkeep-static-functions", + "CMAKE_EXE_LINKER_FLAGS_COVERAGE": "--coverage", + "CMAKE_SHARED_LINKER_FLAGS_COVERAGE": "--coverage" + } + }, + { + "name": "ci-coverage", + "inherits": ["coverage-unix", "dev-mode"], + "cacheVariables": { + "COVERAGE_HTML_COMMAND": "" + } + }, + { + "name": "ci-sanitize", + "binaryDir": "${sourceDir}/build/sanitize", + "inherits": ["ci-unix", "dev-mode"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Sanitize", + "CMAKE_CXX_FLAGS_SANITIZE": "-O2 -g -fsanitize=address,undefined -fno-omit-frame-pointer -fno-common" + } + }, + { + "name": "ci-build", + "binaryDir": "${sourceDir}/build", + "hidden": true + }, + { + "name": "ci-macos", + "inherits": ["ci-build", "ci-unix", "dev-mode"] + }, + { + "name": "ci-ubuntu", + "inherits": ["ci-build", "ci-unix", "clang-tidy", "cppcheck", "dev-mode"] + }, + { + "name": "ci-windows", + "inherits": ["ci-build", "ci-win64", "dev-mode"] + } + ] +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..d120231 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,5 @@ +# Code of Conduct + +* You will be judged by your contributions first, and your sense of humor + second. +* Nobody owes you anything. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..10cccf3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,19 @@ +# Contributing + + + +## Code of Conduct + +Please see the [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) document. + +## Getting started + +Helpful notes for developers can be found in the [`HACKING.md`](HACKING.md) +document. + +In addition to he above, if you use the presets file as instructed, then you +should NOT check it into source control, just as the CMake documentation +suggests. diff --git a/HACKING.md b/HACKING.md new file mode 100644 index 0000000..f94530d --- /dev/null +++ b/HACKING.md @@ -0,0 +1,127 @@ +# Hacking + +Here is some wisdom to help you build and test this project as a developer and +potential contributor. + +If you plan to contribute, please read the [CONTRIBUTING](CONTRIBUTING.md) +guide. + +## Developer mode + +Build system targets that are only useful for developers of this project are +hidden if the `crc32_DEVELOPER_MODE` option is disabled. Enabling this +option makes tests and other developer targets and options available. Not +enabling this option means that you are a consumer of this project and thus you +have no need for these targets and options. + +Developer mode is always set to on in CI workflows. + +### Presets + +This project makes use of [presets][1] to simplify the process of configuring +the project. As a developer, you are recommended to always have the [latest +CMake version][2] installed to make use of the latest Quality-of-Life +additions. + +You have a few options to pass `crc32_DEVELOPER_MODE` to the configure +command, but this project prefers to use presets. + +As a developer, you should create a `CMakeUserPresets.json` file at the root of +the project: + +```json +{ + "version": 1, + "cmakeMinimumRequired": { + "major": 3, + "minor": 14, + "patch": 0 + }, + "configurePresets": [ + { + "name": "dev", + "binaryDir": "${sourceDir}/build/dev", + "inherits": ["dev-mode", "ci-"] + } + ] +} +``` + +You should replace `` in your newly created presets file with the name of +the operating system you have, which may be `win64` or `unix`. You can see what +these correspond to in the [`CMakePresets.json`](CMakePresets.json) file. + +`CMakeUserPresets.json` is also the perfect place in which you can put all +sorts of things that you would otherwise want to pass to the configure command +in the terminal. + +Full example with Ubuntu (Linux): + +```json +{ + "version": 1, + "cmakeMinimumRequired": { + "major": 3, + "minor": 14, + "patch": 0 + }, + "configurePresets": [ + { + "name": "static-analyzers", + "hidden": true, + "inherits": ["clang-tidy", "cppcheck"] + }, + { + "name": "dev-common", + "hidden": true, + "inherits": ["static-analyzers", "dev-mode"], + "cacheVariables": { + "BUILD_MCSS_DOCS": "ON" + } + }, + { + "name": "dev-unix", + "binaryDir": "${sourceDir}/build/dev-unix", + "inherits": ["dev-common", "ci-unix"] + }, + { + "name": "dev-win64", + "binaryDir": "${sourceDir}/build/dev-win64", + "inherits": ["dev-common", "ci-win64"] + }, + { + "name": "dev", + "binaryDir": "${sourceDir}/build/dev", + "inherits": "dev-unix" + }, + { + "name": "dev-coverage", + "binaryDir": "${sourceDir}/build/coverage", + "inherits": ["dev-mode", "coverage-unix"] + } + ] +} +``` + +### Configure, build and test + +If you followed the above instructions, then you can configure, build and test +the project respectively with the following commands from the project root on +Windows: + +```sh +cmake --preset=dev +cmake --build build/dev --config Release +cd build/dev && ctest -C Release +``` + +And here is the same on a Unix based system (Linux, macOS): + +```sh +cmake --preset=dev +cmake --build build/dev +cd build/dev && ctest +``` + +[1]: https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html +[2]: https://cmake.org/download/ diff --git a/README.md b/README.md index be3d25a..8a88e22 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,13 @@ -ThreadPool -========== +# ThreadPool -A simple C++11 Thread Pool implementation. +This is the ThreadPool project. -Basic usage: -```c++ -// create thread pool with 4 worker threads -ThreadPool pool(4); +# Building and installing -// enqueue and store future -auto result = pool.enqueue([](int answer) { return answer; }, 42); +See the [BUILDING](BUILDING.md) document. -// get result from future -std::cout << result.get() << std::endl; +# Contributing -``` +See the [CONTRIBUTING](CONTRIBUTING.md) document. + +# Licensing diff --git a/ThreadPool.h b/ThreadPool.h deleted file mode 100755 index cce4906..0000000 --- a/ThreadPool.h +++ /dev/null @@ -1,306 +0,0 @@ -// -*- C++ -*- -// Copyright (c) 2012-2015 Jakob Progsch -// -// This software is provided 'as-is', without any express or implied -// warranty. In no event will the authors be held liable for any damages -// arising from the use of this software. -// -// Permission is granted to anyone to use this software for any purpose, -// including commercial applications, and to alter it and redistribute it -// freely, subject to the following restrictions: -// -// 1. The origin of this software must not be misrepresented; you must not -// claim that you wrote the original software. If you use this software -// in a product, an acknowledgment in the product documentation would be -// appreciated but is not required. -// -// 2. Altered source versions must be plainly marked as such, and must not be -// misrepresented as being the original software. -// -// 3. This notice may not be removed or altered from any source -// distribution. -// -// Modified for log4cplus, copyright (c) 2014-2015 Václav Zeman. - -#ifndef THREAD_POOL_H_7ea1ee6b_4f17_4c09_b76b_3d44e102400c -#define THREAD_POOL_H_7ea1ee6b_4f17_4c09_b76b_3d44e102400c - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -namespace progschj { - -class ThreadPool { -public: - explicit ThreadPool(std::size_t threads - = (std::max)(2u, std::thread::hardware_concurrency())); - template - auto enqueue(F&& f, Args&&... args) - -> std::future< -#if defined(__cpp_lib_is_invocable) && __cpp_lib_is_invocable >= 201703 - typename std::invoke_result::type -#else - typename std::result_of::type -#endif - >; - void wait_until_empty(); - void wait_until_nothing_in_flight(); - void set_queue_size_limit(std::size_t limit); - void set_pool_size(std::size_t limit); - ~ThreadPool(); - -private: - void start_worker(std::size_t worker_number, - std::unique_lock const &lock); - - // need to keep track of threads so we can join them - std::vector< std::thread > workers; - // target pool size - std::size_t pool_size; - // the task queue - std::queue< std::function > tasks; - // queue length limit - std::size_t max_queue_size = 100000; - // stop signal - bool stop = false; - - // synchronization - std::mutex queue_mutex; - std::condition_variable condition_producers; - std::condition_variable condition_consumers; - - std::mutex in_flight_mutex; - std::condition_variable in_flight_condition; - std::atomic in_flight; - - struct handle_in_flight_decrement - { - ThreadPool & tp; - - handle_in_flight_decrement(ThreadPool & tp_) - : tp(tp_) - { } - - ~handle_in_flight_decrement() - { - std::size_t prev - = std::atomic_fetch_sub_explicit(&tp.in_flight, - std::size_t(1), - std::memory_order_acq_rel); - if (prev == 1) - { - std::unique_lock guard(tp.in_flight_mutex); - tp.in_flight_condition.notify_all(); - } - } - }; -}; - -// the constructor just launches some amount of workers -inline ThreadPool::ThreadPool(std::size_t threads) - : pool_size(threads) - , in_flight(0) -{ - std::unique_lock lock(this->queue_mutex); - for (std::size_t i = 0; i != threads; ++i) - start_worker(i, lock); -} - -// add new work item to the pool -template -auto ThreadPool::enqueue(F&& f, Args&&... args) - -> std::future< -#if defined(__cpp_lib_is_invocable) && __cpp_lib_is_invocable >= 201703 - typename std::invoke_result::type -#else - typename std::result_of::type -#endif - > -{ -#if defined(__cpp_lib_is_invocable) && __cpp_lib_is_invocable >= 201703 - using return_type = typename std::invoke_result::type; -#else - using return_type = typename std::result_of::type; -#endif - - - auto task = std::make_shared< std::packaged_task >( - std::bind(std::forward(f), std::forward(args)...) - ); - - std::future res = task->get_future(); - - std::unique_lock lock(queue_mutex); - if (tasks.size () >= max_queue_size) - // wait for the queue to empty or be stopped - condition_producers.wait(lock, - [this] - { - return tasks.size () < max_queue_size - || stop; - }); - - // don't allow enqueueing after stopping the pool - if (stop) - throw std::runtime_error("enqueue on stopped ThreadPool"); - - tasks.emplace([task](){ (*task)(); }); - std::atomic_fetch_add_explicit(&in_flight, - std::size_t(1), - std::memory_order_relaxed); - condition_consumers.notify_one(); - - return res; -} - - -// the destructor joins all threads -inline ThreadPool::~ThreadPool() -{ - std::unique_lock lock(queue_mutex); - stop = true; - pool_size = 0; - condition_consumers.notify_all(); - condition_producers.notify_all(); - condition_consumers.wait(lock, [this]{ return this->workers.empty(); }); - assert(in_flight == 0); -} - -inline void ThreadPool::wait_until_empty() -{ - std::unique_lock lock(this->queue_mutex); - this->condition_producers.wait(lock, - [this]{ return this->tasks.empty(); }); -} - -inline void ThreadPool::wait_until_nothing_in_flight() -{ - std::unique_lock lock(this->in_flight_mutex); - this->in_flight_condition.wait(lock, - [this]{ return this->in_flight == 0; }); -} - -inline void ThreadPool::set_queue_size_limit(std::size_t limit) -{ - std::unique_lock lock(this->queue_mutex); - - if (stop) - return; - - std::size_t const old_limit = max_queue_size; - max_queue_size = (std::max)(limit, std::size_t(1)); - if (old_limit < max_queue_size) - condition_producers.notify_all(); -} - -inline void ThreadPool::set_pool_size(std::size_t limit) -{ - if (limit < 1) - limit = 1; - - std::unique_lock lock(this->queue_mutex); - - if (stop) - return; - - std::size_t const old_size = pool_size; - assert(this->workers.size() >= old_size); - - pool_size = limit; - if (pool_size > old_size) - { - // create new worker threads - // it is possible that some of these are still running because - // they have not stopped yet after a pool size reduction, such - // workers will just keep running - for (std::size_t i = old_size; i != pool_size; ++i) - start_worker(i, lock); - } - else if (pool_size < old_size) - // notify all worker threads to start downsizing - this->condition_consumers.notify_all(); -} - -inline void ThreadPool::start_worker( - std::size_t worker_number, std::unique_lock const &lock) -{ - assert(lock.owns_lock() && lock.mutex() == &this->queue_mutex); - assert(worker_number <= this->workers.size()); - - auto worker_func = - [this, worker_number] - { - for(;;) - { - std::function task; - bool notify; - - { - std::unique_lock lock(this->queue_mutex); - this->condition_consumers.wait(lock, - [this, worker_number]{ - return this->stop || !this->tasks.empty() - || pool_size < worker_number + 1; }); - - // deal with downsizing of thread pool or shutdown - if ((this->stop && this->tasks.empty()) - || (!this->stop && pool_size < worker_number + 1)) - { - // detach this worker, effectively marking it stopped - this->workers[worker_number].detach(); - // downsize the workers vector as much as possible - while (this->workers.size() > pool_size - && !this->workers.back().joinable()) - this->workers.pop_back(); - // if this is was last worker, notify the destructor - if (this->workers.empty()) - this->condition_consumers.notify_all(); - return; - } - else if (!this->tasks.empty()) - { - task = std::move(this->tasks.front()); - this->tasks.pop(); - notify = this->tasks.size() + 1 == max_queue_size - || this->tasks.empty(); - } - else - continue; - } - - handle_in_flight_decrement guard(*this); - - if (notify) - { - std::unique_lock lock(this->queue_mutex); - condition_producers.notify_all(); - } - - task(); - } - }; - - if (worker_number < this->workers.size()) { - std::thread & worker = this->workers[worker_number]; - // start only if not already running - if (!worker.joinable()) { - worker = std::thread(worker_func); - } - } else - this->workers.push_back(std::thread(worker_func)); -} - -} // namespace progschj - -#endif // THREAD_POOL_H_7ea1ee6b_4f17_4c09_b76b_3d44e102400c diff --git a/cmake/coverage.cmake b/cmake/coverage.cmake new file mode 100644 index 0000000..c89cc16 --- /dev/null +++ b/cmake/coverage.cmake @@ -0,0 +1,33 @@ +# ---- Variables ---- + +# We use variables separate from what CTest uses, because those have +# customization issues +set( + COVERAGE_TRACE_COMMAND + lcov -c -q + -o "${PROJECT_BINARY_DIR}/coverage.info" + -d "${PROJECT_BINARY_DIR}" + --include "${PROJECT_SOURCE_DIR}/*" + CACHE STRING + "; separated command to generate a trace for the 'coverage' target" +) + +set( + COVERAGE_HTML_COMMAND + genhtml --legend -f -q + "${PROJECT_BINARY_DIR}/coverage.info" + -p "${PROJECT_SOURCE_DIR}" + -o "${PROJECT_BINARY_DIR}/coverage_html" + CACHE STRING + "; separated command to generate an HTML report for the 'coverage' target" +) + +# ---- Coverage target ---- + +add_custom_target( + coverage + COMMAND ${COVERAGE_TRACE_COMMAND} + COMMAND ${COVERAGE_HTML_COMMAND} + COMMENT "Generating coverage report" + VERBATIM +) diff --git a/cmake/dev-mode.cmake b/cmake/dev-mode.cmake new file mode 100644 index 0000000..cfffaa6 --- /dev/null +++ b/cmake/dev-mode.cmake @@ -0,0 +1,25 @@ +include(cmake/folders.cmake) + +include(CTest) +if(BUILD_TESTING) + add_subdirectory(test) +endif() + +option(BUILD_MCSS_DOCS "Build documentation using Doxygen and m.css" OFF) +if(BUILD_MCSS_DOCS) + include(cmake/docs.cmake) +endif() + +option(ENABLE_COVERAGE "Enable coverage support separate from CTest's" OFF) +if(ENABLE_COVERAGE) + include(cmake/coverage.cmake) +endif() + +if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") + include(cmake/open-cpp-coverage.cmake OPTIONAL) +endif() + +include(cmake/lint-targets.cmake) +include(cmake/spell-targets.cmake) + +add_folders(Project) diff --git a/cmake/docs.cmake b/cmake/docs.cmake new file mode 100644 index 0000000..e2b4723 --- /dev/null +++ b/cmake/docs.cmake @@ -0,0 +1,50 @@ +# ---- Redefine docs_early_return ---- + +# This function must be a macro, so the return() takes effect in the calling +# scope. This prevents other targets from being available and potentially +# requiring dependencies. This cuts down on the time it takes to generate +# documentation in CI. +macro(docs_early_return) + return() +endmacro() + +# ---- Dependencies ---- + +include(FetchContent) +FetchContent_Declare( + mcss URL + https://github.com/friendlyanon/m.css/releases/download/release-1/mcss.zip + URL_MD5 00cd2757ebafb9bcba7f5d399b3bec7f + SOURCE_DIR "${PROJECT_BINARY_DIR}/mcss" + UPDATE_DISCONNECTED YES +) +FetchContent_MakeAvailable(mcss) + +find_package(Python3 3.6 REQUIRED) + +# ---- Declare documentation target ---- + +set( + DOXYGEN_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/docs" + CACHE PATH "Path for the generated Doxygen documentation" +) + +set(working_dir "${PROJECT_BINARY_DIR}/docs") + +foreach(file IN ITEMS Doxyfile conf.py) + configure_file("docs/${file}.in" "${working_dir}/${file}" @ONLY) +endforeach() + +set(mcss_script "${mcss_SOURCE_DIR}/documentation/doxygen.py") +set(config "${working_dir}/conf.py") + +add_custom_target( + docs + COMMAND "${CMAKE_COMMAND}" -E remove_directory + "${DOXYGEN_OUTPUT_DIRECTORY}/html" + "${DOXYGEN_OUTPUT_DIRECTORY}/xml" + COMMAND "${Python3_EXECUTABLE}" "${mcss_script}" "${config}" + COMMENT "Building documentation using Doxygen and m.css" + WORKING_DIRECTORY "${working_dir}" + VERBATIM +) diff --git a/cmake/folders.cmake b/cmake/folders.cmake new file mode 100644 index 0000000..da7bd33 --- /dev/null +++ b/cmake/folders.cmake @@ -0,0 +1,21 @@ +set_property(GLOBAL PROPERTY USE_FOLDERS YES) + +# Call this function at the end of a directory scope to assign a folder to +# targets created in that directory. Utility targets will be assigned to the +# UtilityTargets folder, otherwise to the ${name}Targets folder. If a target +# already has a folder assigned, then that target will be skipped. +function(add_folders name) + get_property(targets DIRECTORY PROPERTY BUILDSYSTEM_TARGETS) + foreach(target IN LISTS targets) + get_property(folder TARGET "${target}" PROPERTY FOLDER) + if(DEFINED folder) + continue() + endif() + set(folder Utility) + get_property(type TARGET "${target}" PROPERTY TYPE) + if(NOT type STREQUAL "UTILITY") + set(folder "${name}") + endif() + set_property(TARGET "${target}" PROPERTY FOLDER "${folder}Targets") + endforeach() +endfunction() diff --git a/cmake/install-config.cmake b/cmake/install-config.cmake new file mode 100644 index 0000000..600b31a --- /dev/null +++ b/cmake/install-config.cmake @@ -0,0 +1 @@ +include("${CMAKE_CURRENT_LIST_DIR}/ThreadPoolTargets.cmake") diff --git a/cmake/install-rules.cmake b/cmake/install-rules.cmake new file mode 100644 index 0000000..343732b --- /dev/null +++ b/cmake/install-rules.cmake @@ -0,0 +1,61 @@ +if(PROJECT_IS_TOP_LEVEL) + set(CMAKE_INSTALL_INCLUDEDIR include/ThreadPool CACHE PATH "") +endif() + +# Project is configured with no languages, so tell GNUInstallDirs the lib dir +set(CMAKE_INSTALL_LIBDIR lib CACHE PATH "") + +include(CMakePackageConfigHelpers) +include(GNUInstallDirs) + +# find_package() call for consumers to find this project +set(package ThreadPool) + +install( + DIRECTORY include/ + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" + COMPONENT ThreadPool_Development +) + +install( + TARGETS ThreadPool_ThreadPool + EXPORT ThreadPoolTargets + INCLUDES DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" +) + +write_basic_package_version_file( + "${package}ConfigVersion.cmake" + COMPATIBILITY SameMajorVersion + ARCH_INDEPENDENT +) + +# Allow package maintainers to freely override the path for the configs +set( + ThreadPool_INSTALL_CMAKEDIR "${CMAKE_INSTALL_DATADIR}/${package}" + CACHE PATH "CMake package config location relative to the install prefix" +) +mark_as_advanced(ThreadPool_INSTALL_CMAKEDIR) + +install( + FILES cmake/install-config.cmake + DESTINATION "${ThreadPool_INSTALL_CMAKEDIR}" + RENAME "${package}Config.cmake" + COMPONENT ThreadPool_Development +) + +install( + FILES "${PROJECT_BINARY_DIR}/${package}ConfigVersion.cmake" + DESTINATION "${ThreadPool_INSTALL_CMAKEDIR}" + COMPONENT ThreadPool_Development +) + +install( + EXPORT ThreadPoolTargets + NAMESPACE ThreadPool:: + DESTINATION "${ThreadPool_INSTALL_CMAKEDIR}" + COMPONENT ThreadPool_Development +) + +if(PROJECT_IS_TOP_LEVEL) + include(CPack) +endif() diff --git a/cmake/lint-targets.cmake b/cmake/lint-targets.cmake new file mode 100644 index 0000000..244d521 --- /dev/null +++ b/cmake/lint-targets.cmake @@ -0,0 +1,34 @@ +set( + FORMAT_PATTERNS + source/*.cpp source/*.hpp + include/*.hpp + test/*.cpp test/*.hpp + example/*.cpp example/*.hpp + CACHE STRING + "; separated patterns relative to the project source dir to format" +) + +set(FORMAT_COMMAND clang-format CACHE STRING "Formatter to use") + +add_custom_target( + format-check + COMMAND "${CMAKE_COMMAND}" + -D "FORMAT_COMMAND=${FORMAT_COMMAND}" + -D "PATTERNS=${FORMAT_PATTERNS}" + -P "${PROJECT_SOURCE_DIR}/cmake/lint.cmake" + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + COMMENT "Linting the code" + VERBATIM +) + +add_custom_target( + format-fix + COMMAND "${CMAKE_COMMAND}" + -D "FORMAT_COMMAND=${FORMAT_COMMAND}" + -D "PATTERNS=${FORMAT_PATTERNS}" + -D FIX=YES + -P "${PROJECT_SOURCE_DIR}/cmake/lint.cmake" + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + COMMENT "Fixing the code" + VERBATIM +) diff --git a/cmake/lint.cmake b/cmake/lint.cmake new file mode 100644 index 0000000..c0d2725 --- /dev/null +++ b/cmake/lint.cmake @@ -0,0 +1,52 @@ +cmake_minimum_required(VERSION 3.14) + +macro(default name) + if(NOT DEFINED "${name}") + set("${name}" "${ARGN}") + endif() +endmacro() + +default(FORMAT_COMMAND clang-format) +default( + PATTERNS + source/*.cpp source/*.hpp + include/*.hpp + test/*.cpp test/*.hpp + example/*.cpp example/*.hpp +) +default(FIX NO) + +set(flag --output-replacements-xml) +set(args OUTPUT_VARIABLE output) +if(FIX) + set(flag -i) + set(args "") +endif() + +file(GLOB_RECURSE files ${PATTERNS}) +set(badly_formatted "") +set(output "") +string(LENGTH "${CMAKE_SOURCE_DIR}/" path_prefix_length) + +foreach(file IN LISTS files) + execute_process( + COMMAND "${FORMAT_COMMAND}" --style=file "${flag}" "${file}" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + RESULT_VARIABLE result + ${args} + ) + if(NOT result EQUAL "0") + message(FATAL_ERROR "'${file}': formatter returned with ${result}") + endif() + if(NOT FIX AND output MATCHES "\n ...) +function(windows_set_path TEST) + if(NOT CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") + return() + endif() + + set(path "") + set(glue "") + foreach(target IN LISTS ARGN) + get_target_property(type "${target}" TYPE) + if(type STREQUAL "SHARED_LIBRARY") + set(path "${path}${glue}$") + set(glue "\;") # backslash is important + endif() + endforeach() + if(NOT path STREQUAL "") + set_property(TEST "${TEST}" PROPERTY ENVIRONMENT "PATH=${path}") + endif() +endfunction() diff --git a/docs/Doxyfile.in b/docs/Doxyfile.in new file mode 100644 index 0000000..dc37a2a --- /dev/null +++ b/docs/Doxyfile.in @@ -0,0 +1,32 @@ +# Configuration for Doxygen for use with CMake +# Only options that deviate from the default are included +# To create a new Doxyfile containing all available options, call `doxygen -g` + +# Get Project name and version from CMake +PROJECT_NAME = "@PROJECT_NAME@" +PROJECT_NUMBER = "@PROJECT_VERSION@" + +# Add sources +INPUT = "@PROJECT_SOURCE_DIR@/README.md" "@PROJECT_SOURCE_DIR@/include" "@PROJECT_SOURCE_DIR@/docs/pages" +EXTRACT_ALL = YES +RECURSIVE = YES +OUTPUT_DIRECTORY = "@DOXYGEN_OUTPUT_DIRECTORY@" + +# Use the README as a main page +USE_MDFILE_AS_MAINPAGE = "@PROJECT_SOURCE_DIR@/README.md" + +# set relative include paths +FULL_PATH_NAMES = YES +STRIP_FROM_PATH = "@PROJECT_SOURCE_DIR@/include" "@PROJECT_SOURCE_DIR@" +STRIP_FROM_INC_PATH = + +# We use m.css to generate the html documentation, so we only need XML output +GENERATE_XML = YES +GENERATE_HTML = NO +GENERATE_LATEX = NO +XML_PROGRAMLISTING = NO +CREATE_SUBDIRS = NO + +# Include all directories, files and namespaces in the documentation +# Disable to include only explicitly documented objects +M_SHOW_UNDOCUMENTED = YES diff --git a/docs/conf.py.in b/docs/conf.py.in new file mode 100644 index 0000000..b81e3d9 --- /dev/null +++ b/docs/conf.py.in @@ -0,0 +1,6 @@ +DOXYFILE = 'Doxyfile' + +LINKS_NAVBAR1 = [ + (None, 'pages', [(None, 'about')]), + (None, 'namespaces', []), +] diff --git a/docs/pages/about.dox b/docs/pages/about.dox new file mode 100644 index 0000000..2efbda9 --- /dev/null +++ b/docs/pages/about.dox @@ -0,0 +1,7 @@ +/** + * @page about About + * @section about-doxygen Doxygen documentation + * This page is auto generated using + * Doxygen, making use of some useful + * special commands. + */ diff --git a/example.cpp b/example.cpp deleted file mode 100644 index 8288cb8..0000000 --- a/example.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include -#include -#include - -#include "ThreadPool.h" - -using namespace progschj; - -int main() -{ - - ThreadPool pool; - std::vector< std::future > results; - - for(int i = 0; i < 8; ++i) { - results.emplace_back( - pool.enqueue([i] { - std::cout << "hello " << i << std::endl; - std::this_thread::sleep_for(std::chrono::seconds(1)); - std::cout << "world " << i << std::endl; - return i*i; - }) - ); - } - - pool.wait_until_empty(); - pool.wait_until_nothing_in_flight (); - - for(auto && result: results) - std::cout << result.get() << ' '; - std::cout << std::endl; - - return 0; -} diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt new file mode 100644 index 0000000..bd4488e --- /dev/null +++ b/example/CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.14) + +project(ThreadPoolExamples CXX) + +include(../cmake/project-is-top-level.cmake) +include(../cmake/folders.cmake) + +if(PROJECT_IS_TOP_LEVEL) + find_package(ThreadPool REQUIRED) +endif() + +add_custom_target(run-examples) + +function(add_example NAME) + add_executable("${NAME}" "${NAME}.cpp") + target_link_libraries("${NAME}" PRIVATE ThreadPool::ThreadPool Threads::Threads) + #target_compile_features("${NAME}" PRIVATE cxx_std_17) + add_custom_target("run_${NAME}" COMMAND "${NAME}" VERBATIM) + add_dependencies("run_${NAME}" "${NAME}") + add_dependencies(run-examples "run_${NAME}") +endfunction() + +set(CMAKE_THREAD_PREFER_PTHREAD TRUE) +set(THREADS_PREFER_PTHREAD_FLAG TRUE) +find_package(Threads REQUIRED) + +add_example(example_1) +add_example(example_2) +add_example(function_example) + +add_folders(Example) diff --git a/example/example_1.cpp b/example/example_1.cpp new file mode 100644 index 0000000..66d4e2f --- /dev/null +++ b/example/example_1.cpp @@ -0,0 +1,15 @@ +#include + +#include "ThreadPool/ThreadPool.hpp" + +auto main() -> int +{ + // create thread pool with 4 worker threads + progschj::ThreadPool pool(4); + + // enqueue and store future + auto result = pool.enqueue([](int answer) { return answer; }, 42); + + // get result from future + std::cout << result.get() << std::endl; +} diff --git a/example/example_2.cpp b/example/example_2.cpp new file mode 100644 index 0000000..9c70981 --- /dev/null +++ b/example/example_2.cpp @@ -0,0 +1,33 @@ +#include +#include +#include +#include +#include + +#include "ThreadPool/ThreadPool.hpp" + +auto main() -> int +{ + progschj::ThreadPool pool(4); + std::vector > results; + + for (int i = 0; i < 8; ++i) { + results.emplace_back(pool.enqueue( + [i] + { + std::cout << "hello " << i << std::endl; + std::this_thread::sleep_for(std::chrono::seconds(1)); + std::cout << "world " << i << std::endl; + return i * i; + })); + } + + pool.wait_until_empty(); + pool.wait_until_nothing_in_flight(); + + for (auto&& result : results) + std::cout << result.get() << ' '; + std::cout << std::endl; + + return 0; +} diff --git a/example/function_example.cpp b/example/function_example.cpp new file mode 100644 index 0000000..eb3bc1c --- /dev/null +++ b/example/function_example.cpp @@ -0,0 +1,31 @@ +#include +#include +#include + +#include "ThreadPool/ThreadPool.hpp" + +struct Processor +{ + auto operator()(int value, int multi) -> int + { + return value * multi; + } +}; + +auto main() -> int +{ + // create thread pool + progschj::ThreadPool pool(std::thread::hardware_concurrency()); + + std::vector> results {}; + + // enqueue and store future + for (int i = 0; i < 8; ++i) { + results.emplace_back(pool.enqueue(Processor(), i, i * 2)); + } + + // get result from future + for (auto&& result : results) { + std::cout << result.get() << std::endl; + } +} diff --git a/include/ThreadPool/ThreadPool.hpp b/include/ThreadPool/ThreadPool.hpp new file mode 100644 index 0000000..ca1e237 --- /dev/null +++ b/include/ThreadPool/ThreadPool.hpp @@ -0,0 +1,284 @@ +// -*- C++ -*- +// Copyright (c) 2012-2015 Jakob Progsch +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source +// distribution. +// +// Modified for log4cplus, copyright (c) 2014-2015 Václav Zeman. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace progschj +{ +class ThreadPool +{ +public: + explicit ThreadPool(std::size_t threads = + (std::max)(2u, std::thread::hardware_concurrency())); + template + auto enqueue(F&& f, Args&&... args) -> std::future< +#if defined(__cpp_lib_is_invocable) && __cpp_lib_is_invocable >= 201703 + typename std::invoke_result::type +#else + typename std::result_of::type +#endif + >; + void wait_until_empty(); + void wait_until_nothing_in_flight(); + void set_queue_size_limit(std::size_t limit); + void set_pool_size(std::size_t limit); + ~ThreadPool(); + +private: + void start_worker(std::size_t worker_number, + std::unique_lock const& lock); + + // need to keep track of threads so we can join them + std::vector workers; + // target pool size + std::size_t pool_size; + // the task queue + std::queue > tasks; + // queue length limit + std::size_t max_queue_size = 1000 * 1000 * 1000; + // stop signal + bool stop = false; + + // synchronization + std::mutex queue_mutex; + std::condition_variable condition_producers; + std::condition_variable condition_consumers; + + std::mutex in_flight_mutex; + std::condition_variable in_flight_condition; + std::atomic in_flight; + + struct handle_in_flight_decrement + { + ThreadPool& tp; + + handle_in_flight_decrement(ThreadPool& tp_) + : tp(tp_) + { + } + + ~handle_in_flight_decrement() + { + std::size_t prev = std::atomic_fetch_sub_explicit( + &tp.in_flight, std::size_t(1), std::memory_order_acq_rel); + if (prev == 1) { + std::unique_lock guard(tp.in_flight_mutex); + tp.in_flight_condition.notify_all(); + } + } + }; +}; + +// the constructor just launches some amount of workers +inline ThreadPool::ThreadPool(std::size_t threads) + : pool_size(threads) + , in_flight(0) +{ + std::unique_lock lock(this->queue_mutex); + for (std::size_t i = 0; i != threads; ++i) + start_worker(i, lock); +} + +// add new work item to the pool +template +auto ThreadPool::enqueue(F&& f, Args&&... args) -> std::future< +#if defined(__cpp_lib_is_invocable) && __cpp_lib_is_invocable >= 201703 + typename std::invoke_result::type +#else + typename std::result_of::type +#endif + > +{ +#if defined(__cpp_lib_is_invocable) && __cpp_lib_is_invocable >= 201703 + using return_type = typename std::invoke_result::type; +#else + using return_type = typename std::result_of::type; +#endif + + auto task = std::make_shared >( + std::bind(std::forward(f), std::forward(args)...)); + + std::future res = task->get_future(); + + std::unique_lock lock(queue_mutex); + if (tasks.size() >= max_queue_size) + // wait for the queue to empty or be stopped + condition_producers.wait( + lock, [this] { return tasks.size() < max_queue_size || stop; }); + + // don't allow enqueueing after stopping the pool + if (stop) + throw std::runtime_error("enqueue on stopped ThreadPool"); + + tasks.emplace([task]() { (*task)(); }); + std::atomic_fetch_add_explicit( + &in_flight, std::size_t(1), std::memory_order_relaxed); + condition_consumers.notify_one(); + + return res; +} + +// the destructor joins all threads +inline ThreadPool::~ThreadPool() +{ + std::unique_lock lock(queue_mutex); + stop = true; + pool_size = 0; + condition_consumers.notify_all(); + condition_producers.notify_all(); + condition_consumers.wait(lock, [this] { return this->workers.empty(); }); + assert(in_flight == 0); +} + +inline void ThreadPool::wait_until_empty() +{ + std::unique_lock lock(this->queue_mutex); + this->condition_producers.wait(lock, [this] { return this->tasks.empty(); }); +} + +inline void ThreadPool::wait_until_nothing_in_flight() +{ + std::unique_lock lock(this->in_flight_mutex); + this->in_flight_condition.wait(lock, [this] { return this->in_flight == 0; }); +} + +inline void ThreadPool::set_queue_size_limit(std::size_t limit) +{ + std::unique_lock lock(this->queue_mutex); + + if (stop) + return; + + std::size_t const old_limit = max_queue_size; + max_queue_size = (std::max)(limit, std::size_t(1)); + if (old_limit < max_queue_size) + condition_producers.notify_all(); +} + +inline void ThreadPool::set_pool_size(std::size_t limit) +{ + if (limit < 1) + limit = 1; + + std::unique_lock lock(this->queue_mutex); + + if (stop) + return; + + std::size_t const old_size = pool_size; + assert(this->workers.size() >= old_size); + + pool_size = limit; + if (pool_size > old_size) { + // create new worker threads + // it is possible that some of these are still running because + // they have not stopped yet after a pool size reduction, such + // workers will just keep running + for (std::size_t i = old_size; i != pool_size; ++i) + start_worker(i, lock); + } else if (pool_size < old_size) + // notify all worker threads to start downsizing + this->condition_consumers.notify_all(); +} + +inline void ThreadPool::start_worker(std::size_t worker_number, + std::unique_lock const& lock) +{ + assert(lock.owns_lock() && lock.mutex() == &this->queue_mutex); + assert(worker_number <= this->workers.size()); + + auto worker_func = [this, worker_number] + { + for (;;) { + std::function task; + bool notify; + + { + std::unique_lock _lock(this->queue_mutex); + this->condition_consumers.wait(_lock, + [this, worker_number] + { + return this->stop + || !this->tasks.empty() + || pool_size < worker_number + 1; + }); + + // deal with downsizing of thread pool or shutdown + if ((this->stop && this->tasks.empty()) + || (!this->stop && pool_size < worker_number + 1)) + { + // detach this worker, effectively marking it stopped + this->workers[worker_number].detach(); + // downsize the workers vector as much as possible + while (this->workers.size() > pool_size + && !this->workers.back().joinable()) + this->workers.pop_back(); + // if this is was last worker, notify the destructor + if (this->workers.empty()) + this->condition_consumers.notify_all(); + return; + } else if (!this->tasks.empty()) { + task = std::move(this->tasks.front()); + this->tasks.pop(); + notify = + this->tasks.size() + 1 == max_queue_size || this->tasks.empty(); + } else + continue; + } + + handle_in_flight_decrement guard(*this); + + if (notify) { + std::unique_lock _lock(this->queue_mutex); + condition_producers.notify_all(); + } + + task(); + } + }; + + if (worker_number < this->workers.size()) { + std::thread& worker = this->workers[worker_number]; + // start only if not already running + if (!worker.joinable()) { + worker = std::thread(worker_func); + } + } else + this->workers.push_back(std::thread(worker_func)); +} + +} // namespace progschj diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..eb5c1d0 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,90 @@ +cmake_minimum_required(VERSION 3.14) + +project(ThreadPoolTests LANGUAGES CXX) + +include(../cmake/project-is-top-level.cmake) +include(../cmake/folders.cmake) +include(../cmake/windows-set-path.cmake) + +if(PROJECT_IS_TOP_LEVEL) + find_package(ThreadPool REQUIRED) + enable_testing() +endif() + +set(CMAKE_THREAD_PREFER_PTHREAD TRUE) +set(THREADS_PREFER_PTHREAD_FLAG TRUE) +find_package(Threads REQUIRED) + +if(NOT WIN32) + include(FetchContent) + + # Externally provided libraries + FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG main) + + FetchContent_Declare( + googlebenchmark + GIT_REPOSITORY https://github.com/google/benchmark.git + GIT_TAG main) + + # set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) + + set(FOLDER_benchmark + "gbenchmark" + CACHE STRING "" FORCE) + set(FOLDER_gtest + "gtest" + CACHE STRING "" FORCE) + set(FOLDER_gmock + "gmock" + CACHE STRING "" FORCE) + set(FOLDER_googletest-distribution + "googletest-distribution" + CACHE STRING "" FORCE) + + # Disable tests on gtest + set(gtest_build_tests + OFF + CACHE BOOL "" FORCE) + set(gtest_build_samples + OFF + CACHE BOOL "" FORCE) + + # Disable tests on google benchmark + set(BENCHMARK_ENABLE_TESTING + OFF + CACHE BOOL "" FORCE) + set(BENCHMARK_ENABLE_WERROR + OFF + CACHE BOOL "" FORCE) + + FetchContent_MakeAvailable(googletest googlebenchmark) + # Lib: gtest_main benchmark::benchmark benchmark::benchmark_main + + # target_compile_features(crc32_test PRIVATE cxx_std_17) + + add_executable(ThreadPool_bench source/ThreadPool_bench.cpp) + target_link_libraries(ThreadPool_bench PRIVATE ThreadPool::ThreadPool Threads::Threads benchmark::benchmark_main) + # windows_set_path(ThreadPool_bench ThreadPool::ThreadPool Threads::Threads benchmark::benchmark_main) + + option(BENCHMARK_TEST "RUN bench test with tests" OFF) + + if(BENCHMARK_TEST) + add_test(NAME ThreadPool_bench COMMAND ThreadPool_bench) + elseif() + message(STATUS "Disable ThreadPool_bench") + endif() + + add_executable(ThreadPool_test source/ThreadPool_test.cpp) + target_link_libraries(ThreadPool_test PRIVATE ThreadPool::ThreadPool Threads::Threads gtest_main) + #target_compile_features(ThreadPool_test PRIVATE cxx_std_17) + add_test(NAME ThreadPool_test COMMAND ThreadPool_test) + + # windows_set_path(crc32_test crc32::crc32 gtest_main) +else() + message(WARNING "Disable tests, only tested on Linux and MacOS x86_64") +endif() + +add_folders(Test) diff --git a/test/source/ThreadPool_bench.cpp b/test/source/ThreadPool_bench.cpp new file mode 100644 index 0000000..d73e08d --- /dev/null +++ b/test/source/ThreadPool_bench.cpp @@ -0,0 +1,37 @@ +#include +#include +#include + +#include "ThreadPool/ThreadPool.hpp" + +#include + +static void thread_pool_bench(benchmark::State& state) +{ + const auto size = state.range(0); + + for (auto _ : state) { + benchmark::DoNotOptimize(size); + + progschj::ThreadPool pool(size); + std::vector > results; + + for (int i = 0; i < size; ++i) { + results.emplace_back(pool.enqueue([i] { return i * i; })); + } + + for (auto&& result : results) { + result.get(); + } + + benchmark::ClobberMemory(); + } + state.SetItemsProcessed(state.iterations()); + state.SetBytesProcessed(state.iterations() * size * sizeof(int)); + + // state.SetLabel("OK"); +} +BENCHMARK(thread_pool_bench) + ->Name("thread_pool_bench") + ->RangeMultiplier(2) + ->Range(1, 128); diff --git a/test/source/ThreadPool_test.cpp b/test/source/ThreadPool_test.cpp new file mode 100644 index 0000000..51e86eb --- /dev/null +++ b/test/source/ThreadPool_test.cpp @@ -0,0 +1,54 @@ +#include +#include +#include + +#include "ThreadPool/ThreadPool.hpp" + +#include "gtest/gtest.h" + +TEST(threadpool, onethread) +{ + progschj::ThreadPool pool(1); + + // enqueue and store future + auto result = pool.enqueue([](int answer) { return answer; }, 42); + + // get result from future + EXPECT_EQ(42, result.get()); +} + +TEST(threadpool, twothread) +{ + progschj::ThreadPool pool(2); + + // enqueue and store future + auto result = pool.enqueue([](int answer) { return answer; }, 128); + + // get result from future + EXPECT_EQ(128, result.get()); +} + +TEST(threadpool, manythread) +{ + progschj::ThreadPool pool(128); + + // enqueue and store future + auto result = pool.enqueue([](int answer) { return answer; }, 256); + + // get result from future + EXPECT_EQ(256, result.get()); +} + +TEST(threadpool, manyworker) +{ + progschj::ThreadPool pool; + std::vector > results; + + for (int i = 0; i < 2048; ++i) { + results.emplace_back(pool.enqueue([i] { return i * i; })); + } + + for (int i = 0; i < 2048; ++i) { + EXPECT_EQ(i * i, results[i].get()); + } +} diff --git a/tools/dockcross-cmake-builder.sh b/tools/dockcross-cmake-builder.sh new file mode 100644 index 0000000..7175ecc --- /dev/null +++ b/tools/dockcross-cmake-builder.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +if (($# >= 1)); then + image_complet=$1 + image=${image_complet%:*} + tag=${image_complet#*:} + build_file=build-$image + shift 1 + + cmake_arg=$* + echo "cmake arg: $cmake_arg" + + echo "Pulling dockcross/$image" + if [ -z "${tag}" ]; then + docker pull "dockcross/$image:latest" + else + docker pull "dockcross/$image:$tag" + fi + + echo "Make script dockcross-$image" + docker run --rm dockcross/"$image" >./dockcross-"$image" + chmod +x ./dockcross-"$image" + + echo "Build $build_file" + ./dockcross-"$image" cmake -B "$build_file" -S . -G Ninja $cmake_arg + ./dockcross-"$image" ninja -C "$build_file" +else + echo "Usage: ${0##*/} " + exit 1 +fi diff --git a/tools/graphic.py b/tools/graphic.py new file mode 100644 index 0000000..fcca1b5 --- /dev/null +++ b/tools/graphic.py @@ -0,0 +1,64 @@ +# Based on work: https://int-i.github.io/python/2021-11-07/matplotlib-google-benchmark-visualization/ + +from argparse import ArgumentParser +from itertools import groupby +from cycler import cycler +from random import randint +import json +import math +import operator +import matplotlib as mpl +import matplotlib.pyplot as plt + + +def generate_color(size): + colors = [] + + for i in range(size): + colors.append('#%06X' % randint(0, 0xFFFFFF)) + + colors = sorted(set(colors), key=colors.index) # Remove all same elements + return colors + + +def extract_label_from_benchmark(benchmark): + bench_full_name = benchmark['name'] + bench_name = bench_full_name.split('/')[0] # Remove all after / + if (bench_name.startswith('BM_')): # Remove if string start with BM_ + return bench_name[3:] # Remove BM_ + else: + return bench_name + + +def extract_size_from_benchmark(benchmark): + bench_name = benchmark['name'] + return bench_name.split('/')[1] # Remove all before / + + +if __name__ == "__main__": + plt.rcParams['figure.figsize'] = [21, 12] + mpl.rcParams['axes.prop_cycle'] = cycler(color=generate_color(200)) + + parser = ArgumentParser() + parser.add_argument('path', help='benchmark result json file') + args = parser.parse_args() + + with open(args.path) as file: + benchmark_result = json.load(file) + benchmarks = benchmark_result['benchmarks'] + elapsed_times = groupby(benchmarks, extract_label_from_benchmark) + for key, group in elapsed_times: + benchmark = list(group) + x = list(map(extract_size_from_benchmark, benchmark)) + y = list(map(operator.itemgetter('bytes_per_second'), benchmark)) + #log_y = list(map(math.log, y)) + plt.plot(x, y, label=key, marker=None) + + plt.grid(color='green', linestyle='--', linewidth=0.2) # Add grid + plt.xlabel('Array size') + plt.ylabel('Gigabyte per second (GB/s)') + plt.title('CRC32 Algorithm Benchmark') + plt.legend() + plt.savefig('benchmark.png', bbox_inches='tight', dpi=300) + # plt.savefig('benchmark.svg') + plt.show() diff --git a/tools/ninja-builder.sh b/tools/ninja-builder.sh new file mode 100644 index 0000000..cacfe56 --- /dev/null +++ b/tools/ninja-builder.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +#export CC=/usr/bin/clang +#export CXX=/usr/bin/clang++ + +#--preset=dev --preset=dev-coverage -D CMAKE_BUILD_TYPE=Release -D CMAKE_CXX_STANDARD=17 + +cmake -S . -B build -G Ninja $* --preset=dev-coverage -D CMAKE_BUILD_TYPE=Release -D CMAKE_CXX_STANDARD=17 -D BENCHMARK_TEST=ON + +ninja -C build + +ctest --verbose --parallel $(nproc) --test-dir build From 1ee6e3023d3f68fb7dc677478e29a786b4c152a3 Mon Sep 17 00:00:00 2001 From: Bensuperpc Date: Thu, 25 Nov 2021 23:06:37 +0100 Subject: [PATCH 2/2] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8a88e22..1052141 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ThreadPool -This is the ThreadPool project. +A simple C++11 and C++17 Thread Pool implementation. # Building and installing