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..1052141 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,13 @@ -ThreadPool -========== +# ThreadPool -A simple C++11 Thread Pool implementation. +A simple C++11 and C++17 Thread Pool implementation. -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