diff --git a/.github/workflows/macos_arm.yml b/.github/workflows/macos_arm.yml index 477b6fb..fd600fe 100644 --- a/.github/workflows/macos_arm.yml +++ b/.github/workflows/macos_arm.yml @@ -14,10 +14,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup CMake and Ninja - uses: lukka/get-cmake@v3.21.1 + uses: lukka/get-cmake@v4.2.0 - name: Check Clang Version run: clang++ --version @@ -33,4 +33,4 @@ jobs: - name: Test working-directory: build - run: ctest \ No newline at end of file + run: ctest diff --git a/.github/workflows/macos_x86_64.yml b/.github/workflows/macos_x86_64.yml index fb7b129..1ff1a0c 100644 --- a/.github/workflows/macos_x86_64.yml +++ b/.github/workflows/macos_x86_64.yml @@ -14,10 +14,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup CMake and Ninja - uses: lukka/get-cmake@v3.21.1 + uses: lukka/get-cmake@v4.2.0 - name: Check Clang Version run: clang++ --version @@ -33,4 +33,4 @@ jobs: - name: Test working-directory: build - run: ctest \ No newline at end of file + run: ctest diff --git a/.github/workflows/ubuntu_X86_64.yml b/.github/workflows/ubuntu_X86_64.yml index e3e65f6..21bbeca 100644 --- a/.github/workflows/ubuntu_X86_64.yml +++ b/.github/workflows/ubuntu_X86_64.yml @@ -13,13 +13,13 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install dependencies run: sudo apt-get update && sudo apt-get install -y libgtest-dev libbenchmark-dev cmake - name: Setup CMake and Ninja - uses: lukka/get-cmake@v3.21.1 + uses: lukka/get-cmake@v4.2.0 - name: Check GCC Version run: g++ --version @@ -44,4 +44,4 @@ jobs: run: ./build/benchmarkbin/ThreadPool_ThreadTask_Benchmark - name: Run ThreadPool Benchmarks - run: ./build/benchmarkbin/ThreadPool_ThreadPool_Benchmark \ No newline at end of file + run: ./build/benchmarkbin/ThreadPool_ThreadPool_Benchmark diff --git a/.github/workflows/ubuntu_arm.yml b/.github/workflows/ubuntu_arm.yml index 8fa587a..0943779 100644 --- a/.github/workflows/ubuntu_arm.yml +++ b/.github/workflows/ubuntu_arm.yml @@ -13,13 +13,13 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install dependencies run: sudo apt-get update && sudo apt-get install -y libgtest-dev libbenchmark-dev cmake - name: Setup CMake and Ninja - uses: lukka/get-cmake@v3.21.1 + uses: lukka/get-cmake@v4.2.0 - name: Check GCC Version run: g++ --version @@ -35,4 +35,4 @@ jobs: - name: Test working-directory: build - run: ctest -LE benchmark --output-on-failure \ No newline at end of file + run: ctest -LE benchmark --output-on-failure diff --git a/.github/workflows/ubuntu_riscv.yml b/.github/workflows/ubuntu_riscv.yml index 3b54b96..becd49b 100644 --- a/.github/workflows/ubuntu_riscv.yml +++ b/.github/workflows/ubuntu_riscv.yml @@ -13,13 +13,13 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install dependencies run: sudo apt-get update && sudo apt-get install -y libgtest-dev libbenchmark-dev cmake - name: Setup CMake and Ninja - uses: lukka/get-cmake@v3.21.1 + uses: lukka/get-cmake@v4.2.0 - name: Check GCC Version run: g++ --version @@ -35,4 +35,4 @@ jobs: - name: Test working-directory: build - run: ctest -LE benchmark --output-on-failure \ No newline at end of file + run: ctest -LE benchmark --output-on-failure diff --git a/.github/workflows/windows_arm.yml b/.github/workflows/windows_arm.yml index 4d7f4d9..f8d4607 100644 --- a/.github/workflows/windows_arm.yml +++ b/.github/workflows/windows_arm.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install vcpkg run: | @@ -35,7 +35,7 @@ jobs: vcpkg integrate install - name: Setup CMake and Ninja - uses: lukka/get-cmake@v3.21.1 + uses: lukka/get-cmake@v4.2.0 - name: Setup MSVC uses: ilammy/msvc-dev-cmd@v1 @@ -80,4 +80,4 @@ jobs: - name: Test working-directory: build - run: ctest -LE benchmark --output-on-failure -C Release \ No newline at end of file + run: ctest -LE benchmark --output-on-failure -C Release diff --git a/.github/workflows/windows_x86_64.yml b/.github/workflows/windows_x86_64.yml index 2c17f86..8f8a160 100644 --- a/.github/workflows/windows_x86_64.yml +++ b/.github/workflows/windows_x86_64.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install vcpkg run: | @@ -37,7 +37,7 @@ jobs: vcpkg integrate install - name: Setup CMake and Ninja - uses: lukka/get-cmake@v3.21.1 + uses: lukka/get-cmake@v4.2.0 - name: Setup MSVC uses: ilammy/msvc-dev-cmd@v1 diff --git a/CMakeLists.txt b/CMakeLists.txt index 2b6046a..fb84838 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,7 +9,7 @@ endif() # Get the name of the folder and use it as the project name get_filename_component(PARENT_DIR_NAME ${CMAKE_CURRENT_SOURCE_DIR} NAME) string(REPLACE " " "_" PARENT_DIR_NAME ${PARENT_DIR_NAME}) -project(${PARENT_DIR_NAME} VERSION 2.0.0 DESCRIPTION "A Threadpool function library" LANGUAGES CXX) +project(${PARENT_DIR_NAME} VERSION 2.1.0 DESCRIPTION "A Threadpool function library" LANGUAGES CXX) #------------------------------------------------------------------------------------------ # Set the C++ standard set(CMAKE_CXX_STANDARD 20) diff --git a/benchmark/ThreadPoolBenchmark.cpp b/benchmark/ThreadPoolBenchmark.cpp index 8fded6f..ef409bb 100644 --- a/benchmark/ThreadPoolBenchmark.cpp +++ b/benchmark/ThreadPoolBenchmark.cpp @@ -43,6 +43,28 @@ static void BM_ThreadPool_ExecuteTask(benchmark::State& state) { } BENCHMARK(BM_ThreadPool_ExecuteTask)->RangeMultiplier(2)->Range(4, 64)->Complexity(benchmark::oAuto); +// Benchmark for enqueueing void tasks (asynchronous fire-and-forget) +static void BM_ThreadPool_VoidAsync(benchmark::State& state) { + ThreadPool::ThreadPool pool(state.range(0)); + for (auto _ : state) { + pool.queue([]() {}); + } + state.SetComplexityN(state.range(0)); +} +BENCHMARK(BM_ThreadPool_VoidAsync)->RangeMultiplier(2)->Range(4, 64)->Complexity(benchmark::oAuto); + +// Benchmark for enqueueing void tasks (synchronous with future) +static void BM_ThreadPool_VoidSync(benchmark::State& state) { + ThreadPool::ThreadPool pool(state.range(0)); + for (auto _ : state) { + auto fut = pool.queue([]() {}); + benchmark::DoNotOptimize(fut.valid()); + fut.wait(); + } + state.SetComplexityN(state.range(0)); +} +BENCHMARK(BM_ThreadPool_VoidSync)->RangeMultiplier(2)->Range(4, 64)->Complexity(benchmark::oAuto); + // Benchmark for handling a burst of tasks static void BM_ThreadPool_BurstTasks(benchmark::State& state) { ThreadPool::ThreadPool pool(state.range(0)); @@ -71,6 +93,25 @@ static void BM_ThreadPool_PriorityQueueTask(benchmark::State& state) { } BENCHMARK(BM_ThreadPool_PriorityQueueTask)->RangeMultiplier(2)->Range(4, 64)->Complexity(benchmark::oAuto); +// Benchmark for enum-to-string conversions +static void BM_ThreadMode_NameLookup(benchmark::State& state) { + const auto mode = state.range(0) == 0 ? ThreadPool::ThreadMode::STANDARD : ThreadPool::ThreadMode::PRIORITY; + for (auto _ : state) { + benchmark::DoNotOptimize(ThreadPool::ThreadMode_name(mode)); + } + state.SetComplexityN(1); +} +BENCHMARK(BM_ThreadMode_NameLookup)->DenseRange(0, 1); + +static void BM_ThreadSynchronization_NameLookup(benchmark::State& state) { + const auto sync_mode = state.range(0) == 0 ? ThreadPool::ThreadSynchronization::ASYNCHRONOUS : ThreadPool::ThreadSynchronization::SYNCHRONOUS; + for (auto _ : state) { + benchmark::DoNotOptimize(ThreadPool::ThreadSynchronization_name(sync_mode)); + } + state.SetComplexityN(1); +} +BENCHMARK(BM_ThreadSynchronization_NameLookup)->DenseRange(0, 1); + BENCHMARK_MAIN(); /* diff --git a/example/main.cpp b/example/main.cpp index 9ede80e..b44c31a 100644 --- a/example/main.cpp +++ b/example/main.cpp @@ -100,6 +100,26 @@ int main(void) { _threads.queue([](int value) { std::cout << "ThreadMode::STANDARD Print Value:[" << value << "]" << std::endl; }, i); } } + { + // Demonstrate synchronous void task submission with a waitable future. + ThreadPool::ThreadPool _threads(_size); + std::atomic counter{0}; + + std::vector> futures; + futures.reserve(_size); + for (size_t i = 0; i < _size; ++i) { + auto fut = _threads.queue([&counter]() { + counter.fetch_add(1, std::memory_order_relaxed); + }); + futures.emplace_back(std::move(fut)); + } + + for (auto& fut : futures) { + fut.wait(); + } + + std::cout << "Synchronous void tasks completed. Count:" << counter.load() << std::endl; + } { ThreadPool::ThreadPool _threads(_size); std::vector> results; @@ -167,4 +187,4 @@ int main(void) { } return 0; -} \ No newline at end of file +} diff --git a/include/ThreadMode.hpp b/include/ThreadMode.hpp index 0eaeb48..a3f7fa2 100644 --- a/include/ThreadMode.hpp +++ b/include/ThreadMode.hpp @@ -75,5 +75,52 @@ namespace ThreadPool { //-------------------------------------------------------------- }// end constexpr std::string_view to_string(ThreadMode mode) //-------------------------------------------------------------- + /** + * @enum ThreadSynchronization + * @brief Enum class to specify synchronization preference for void tasks. + * + * @details Use this to choose whether void-returning tasks should expose a future to callers. + * `ASYNCHRONOUS` keeps void tasks fire-and-forget, while `SYNCHRONOUS` exposes a future so callers can wait. + * + * @code + * // Fire-and-forget void task + * pool.queue([] { work }); + * + * // Void task that can be waited on + * auto fut = pool.queue([] { work }); + * fut.wait(); + * @endcode + */ + enum class ThreadSynchronization : bool { + ASYNCHRONOUS = false, + SYNCHRONOUS = true + }; // end enum class ThreadSynchronization + //-------------------------------------------------------------- + /** + * @brief Converts the ThreadSynchronization enum to a string representation. + * + * @details This helper is useful for logging and debugging synchronization choices at runtime. + * @param mode The ThreadSynchronization enum value to convert. + * + * @return std::string_view A string representation of the ThreadSynchronization enum value. + * + * @example + * @code + * constexpr auto sync_mode = ThreadPool::ThreadSynchronization::SYNCHRONOUS; + * constexpr auto name = ThreadPool::ThreadSynchronization_name(sync_mode); + * // name == "SYNCHRONOUS" + * @endcode + */ + constexpr std::string_view ThreadSynchronization_name(const ThreadSynchronization& mode) { + switch (mode) { + case ThreadSynchronization::ASYNCHRONOUS: + return "ASYNCHRONOUS"; + case ThreadSynchronization::SYNCHRONOUS: + return "SYNCHRONOUS"; + default: + return "UNKNOWN"; + } + }// end constexpr std::string_view ThreadSynchronization_name(const ThreadSynchronization& mode) + //-------------------------------------------------------------- } // end namespace ThreadPool -//-------------------------------------------------------------- \ No newline at end of file +//-------------------------------------------------------------- diff --git a/include/ThreadPool.hpp b/include/ThreadPool.hpp index 407a124..73ff2cb 100644 --- a/include/ThreadPool.hpp +++ b/include/ThreadPool.hpp @@ -69,16 +69,14 @@ namespace ThreadPool { * auto result = task.get(); // Will retrieve the result after task execution. * @endcode */ - template requires (static_cast(use_priority_queue)) + template requires (static_cast(use_priority_queue)) class TaskBuilder { //-------------------------------------------------------------- // static_assert(!use_priority_queue, "TaskBuilder can only be used with priority queues disable."); //-------------------------------------------------------------- private: - using ReturnType = std::invoke_result_t, std::decay_t...>; - //-------------------------- - // Dummy struct for void-returning functions - struct VoidType {}; + using ReturnType = std::invoke_result_t, std::decay_t...>; + using TaskReturnType = std::conditional_t, void, ReturnType>; //-------------------------------------------------------------- public: //-------------------------------------------------------------- @@ -121,9 +119,9 @@ namespace ThreadPool { m_submitted(false), m_task(create_task(std::forward(f), std::forward(args)...)) { //-------------------------- - if constexpr (!std::is_void_v) { + if constexpr (!std::is_void_v or sync_mode == ThreadSynchronization::SYNCHRONOUS) { m_future.emplace(m_task.get_future()); - }// end if constexpr (!std::is_void_v) + }// end if constexpr (!std::is_void_v or sync_mode == ThreadSynchronization::SYNCHRONOUS) //-------------------------- if (auto_submit) { submit(); @@ -242,46 +240,20 @@ namespace ThreadPool { }// end if (!m_submitted) }// end void submit(void) //-------------------------- - /** - * @brief This version of the `get_future` method is explicitly deleted for tasks with a void return type. - * - * @details Tasks with a void return type do not have an associated future because there's no result to retrieve. - * Attempting to call this method for such tasks will result in a compilation error. - * - * @tparam T Type of the return value of the task. Defaults to the ReturnType of the task function. - * - * @throws This method is deleted and will cause a compilation error if called. - */ - template - std::enable_if_t, void> get_future(void) = delete; - //-------------------------- /** * @brief Retrieves the future associated with this task, allowing the caller to get the result once it's available. * - * @details This method provides access to the future associated with the task encapsulated by this TaskBuilder. This future - * can be used to retrieve the result of the task once it has been executed by a thread in the ThreadPool. + * @details For non-void tasks this always exposes the underlying future. For void tasks, the future is only + * available when the TaskBuilder was instantiated with ThreadSynchronization::SYNCHRONOUS. * - * @tparam T Type of the return value of the task. Defaults to the ReturnType of the task function. + * @tparam T Type of the return value of the task. Defaults to the TaskReturnType of the task function. * * @return A future associated with the result of the task. The future can be used to retrieve the result. * * @throws std::future_error with an error code of `std::future_errc::no_state` if the future is not available, - * typically because the task has a void return type or the future has already been retrieved. - * - * @example - * ```cpp - * ThreadPool::ThreadPool pool; - * - * // Create and auto-submit a task. - * auto taskBuilder = pool.queue(true, [](int x) { return x*x; }, 5); - * - * // Retrieve the future and get the result. - * std::future resultFuture = taskBuilder.get_future(); - * int result = resultFuture.get(); - * std::cout << "Result: " << result << std::endl; // Prints "Result: 25" - * ``` + * typically because the task has a void return type configured as ASYNCHRONOUS or the future has already been retrieved. */ - template + template std::enable_if_t, std::future> get_future(void) { if(m_future) { return std::move(*m_future); @@ -291,18 +263,18 @@ namespace ThreadPool { //-------------------------- }// end std::enable_if_t, std::future> get_future(void) //-------------------------- - /** - * @brief This version of the `get` method is explicitly deleted for tasks with a void return type. - * - * @details Tasks with a void return type do not produce a result. Thus, calling `get` for such tasks is not meaningful. - * Attempting to call this method for tasks with a void return type will result in a compilation error. - * - * @tparam T Type of the return value of the task. Defaults to the ReturnType of the task function. - * - * @throws This method is deleted and will cause a compilation error if called. - */ - template - std::enable_if_t> get(void) = delete; + template + std::enable_if_t and S == ThreadSynchronization::SYNCHRONOUS, std::future> get_future(void) { + if(m_future) { + return std::move(*m_future); + } // end if(m_future) + //-------------------------- + throw std::future_error(std::future_errc::no_state); + //-------------------------- + }// end std::enable_if_t and S == ThreadSynchronization::SYNCHRONOUS, std::future> get_future(void) + //-------------------------- + template + std::enable_if_t and S == ThreadSynchronization::ASYNCHRONOUS, void> get_future(void) = delete; //-------------------------- /** * @brief Retrieves the result of the task once it has been computed by the ThreadPool. @@ -313,7 +285,7 @@ namespace ThreadPool { * * Note: Once the result has been retrieved using this method, it cannot be obtained again using the same TaskBuilder. * - * @tparam T Type of the return value of the task. Defaults to the ReturnType of the task function. + * @tparam T Type of the return value of the task. Defaults to the TaskReturnType of the task function. * * @return The computed result of the task. * @@ -332,12 +304,24 @@ namespace ThreadPool { * std::cout << "Result: " << result << std::endl; // Prints "Result: 25" * ``` */ - template + template std::enable_if_t, T> get(void) { auto res = m_future->get(); m_future.reset(); // Prevent future gets return res; }// end std::enable_if_t, T> get(void) + //-------------------------- + template + std::enable_if_t and S == ThreadSynchronization::SYNCHRONOUS, void> get(void) { + if(!m_future){ + throw std::future_error(std::future_errc::no_state); + }// end if(!m_future) + m_future->get(); + m_future.reset(); + }// end std::enable_if_t and S == ThreadSynchronization::SYNCHRONOUS, void> get(void) + //-------------------------- + template + std::enable_if_t and S == ThreadSynchronization::ASYNCHRONOUS, void> get(void) = delete; //-------------------------------------------------------------- protected: //-------------------------------------------------------------- @@ -375,24 +359,19 @@ namespace ThreadPool { template auto create_task(Func&& func, CArgs&&... capturedArgs) { //-------------------------- - if constexpr (!std::is_void_v) { - //-------------------------- - return std::packaged_task( + if constexpr (!std::is_void_v) { + return std::packaged_task( [f = std::forward(func), ...args = std::forward(capturedArgs)]() mutable { return std::invoke(f, std::forward(args)...); } ); - //-------------------------- } else { - //-------------------------- - return std::packaged_task( + return std::packaged_task( [f = std::forward(func), ...args = std::forward(capturedArgs)]() mutable { std::invoke(f, std::forward(args)...); - return VoidType{}; } ); - //-------------------------- - }// end else if constexpr (!std::is_void_v) + }// end else if constexpr (!std::is_void_v) }// end auto create_task(Func&& func, CArgs&&... capturedArgs) //-------------------------------------------------------------- private: @@ -401,8 +380,8 @@ namespace ThreadPool { uint16_t m_priority; uint8_t m_retries; bool m_submitted; - std::packaged_task, VoidType, ReturnType>()> m_task; - std::optional> m_future; + std::packaged_task m_task; + std::optional> m_future; //-------------------------------------------------------------- };// end class TaskBuilder //-------------------------------------------------------------- @@ -672,6 +651,7 @@ namespace ThreadPool { * * @param auto_submit Automatically submits the task for execution if set to true. * + * @tparam sync_mode Controls whether void-returning tasks expose a future (ThreadSynchronization::SYNCHRONOUS) or remain fire-and-forget. * @tparam F Type of the callable to be executed. * @tparam Args Variadic template for the arguments list of the callable. * @@ -690,10 +670,10 @@ namespace ThreadPool { * auto result = task.get(); // Will wait for the task to complete and retrieve the result. * @endcode */ - template - std::enable_if_t(use_priority_queue), TaskBuilder> queue(bool auto_submit, F&& f, Args&&... args) { + template + std::enable_if_t(use_priority_queue), TaskBuilder> queue(bool auto_submit, F&& f, Args&&... args) { //-------------------------- - return TaskBuilder(*this, auto_submit, std::forward(f), std::forward(args)...); + return TaskBuilder(*this, auto_submit, std::forward(f), std::forward(args)...); //-------------------------- }// end TaskBuilder queue(F&& f, Args&&... args)// end TaskBuilder queue(F&& f, Args&&... args) //-------------------------- @@ -706,6 +686,7 @@ namespace ThreadPool { * * @note The task is submitted automatically to the thread pool and the TaskBuilder's `submit` method is called automatically * + * @tparam sync_mode Controls whether void-returning tasks expose a future (ThreadSynchronization::SYNCHRONOUS) or remain fire-and-forget. * @tparam F Type of the callable to be * @tparam Args Variadic template for the arguments list of the callable. * @@ -724,10 +705,10 @@ namespace ThreadPool { * auto result = task.get(); // Will wait for the task to complete and retrieve the result. * @endcode */ - template - std::enable_if_t(use_priority_queue), TaskBuilder> queue(F&& f, Args&&... args) { + template + std::enable_if_t(use_priority_queue), TaskBuilder> queue(F&& f, Args&&... args) { //-------------------------- - return TaskBuilder(*this, true, std::forward(f), std::forward(args)...); + return TaskBuilder(*this, true, std::forward(f), std::forward(args)...); //-------------------------- }// end TaskBuilder queue(F&& f, Args&&... args)// end TaskBuilder queue(F&& f, Args&&... args) //-------------------------- @@ -770,6 +751,9 @@ namespace ThreadPool { * task for execution in the thread pool. Unlike its counterpart which handles non-void callables, * this function does not return any future, as there is no return value from the callable. * + * Passing ThreadSynchronization::SYNCHRONOUS exposes a std::future so callers can wait for completion. + * + * @tparam sync_mode Controls whether void-returning tasks expose a future (ThreadSynchronization::SYNCHRONOUS) or remain fire-and-forget. * @tparam F Type of the callable to be executed. * @tparam Args Variadic template for the arguments list of the callable. * @@ -780,19 +764,24 @@ namespace ThreadPool { * // The task is enqueued and will output to the console when executed. * @endcode */ - template - std::enable_if_t(use_priority_queue) and std::is_void_v>, void> queue(F&& f, Args&&... args){ + template + std::enable_if_t(use_priority_queue) and std::is_void_v>, std::conditional_t, void>> + queue(F&& f, Args&&... args){ //-------------------------- - enqueue(std::forward(f), std::forward(args)...); + if constexpr (sync_mode == ThreadSynchronization::SYNCHRONOUS) { + return enqueue(std::forward(f), std::forward(args)...); + } else { + enqueue(std::forward(f), std::forward(args)...); + } //-------------------------- }// end TaskBuilder queue(F&& f, Args&&... args) //-------------------------------------------------------------- protected: //-------------------------------------------------------------- - template - std::enable_if_t(use_priority_queue), TaskBuilder> enqueue(bool auto_submit, F&& f, Args&&... args) { + template + std::enable_if_t(use_priority_queue), TaskBuilder> enqueue(bool auto_submit, F&& f, Args&&... args) { //-------------------------- - return TaskBuilder(*this, auto_submit, std::forward(f), std::forward(args)...); + return TaskBuilder(*this, auto_submit, std::forward(f), std::forward(args)...); //-------------------------- }// end TaskBuilder enqueue(F&& f, Args&&... args) //-------------------------------------------------------------- @@ -821,8 +810,8 @@ namespace ThreadPool { //-------------------------- }// end TaskBuilder enqueue(F&& f, Args&&... args) //-------------------------------------------------------------- - template - std::enable_if_t(use_priority_queue) and std::is_void_v>, void> + template + std::enable_if_t(use_priority_queue) and std::is_void_v>, std::conditional_t, void>> enqueue(F&& f, Args&&... args) { //-------------------------- using ReturnType = std::invoke_result_t, std::decay_t...>; @@ -833,12 +822,24 @@ namespace ThreadPool { //-------------------------- auto packagedTaskPtr = std::make_shared>(std::move(taskFn)); //-------------------------- - {// adding task - std::lock_guard lock(m_mutex); - m_tasks.emplace_back([pt = packagedTaskPtr]() { (*pt)(); }); - }// end adding task - //-------------------------- - m_task_available_condition.notify_one(); + if constexpr (sync_mode == ThreadSynchronization::SYNCHRONOUS) { + std::future future = packagedTaskPtr->get_future(); + {// adding task + std::lock_guard lock(m_mutex); + m_tasks.emplace_back([pt = packagedTaskPtr]() { (*pt)(); }); + }// end adding task + //-------------------------- + m_task_available_condition.notify_one(); + //-------------------------- + return future; + } else { + {// adding task + std::lock_guard lock(m_mutex); + m_tasks.emplace_back([pt = packagedTaskPtr]() { (*pt)(); }); + }// end adding task + //-------------------------- + m_task_available_condition.notify_one(); + }// end if constexpr (sync_mode == ThreadSynchronization::SYNCHRONOUS) //-------------------------- }// end enqueue(F&& f, Args&&... args) //-------------------------------------------------------------- @@ -868,7 +869,7 @@ namespace ThreadPool { //-------------------------- }//end void ThreadPool::ThreadPool::create_task(const size_t& number_threads) //-------------------------- - void worker_function(const std::stop_token& stoken){ + void worker_function(const std::stop_token& stoken) { //-------------------------- std::optional id{std::nullopt}; //-------------------------- @@ -1134,4 +1135,4 @@ namespace ThreadPool { };// end class ThreadPool //-------------------------------------------------------------- }//end namespace ThreadPool -//-------------------------------------------------------------- \ No newline at end of file +//-------------------------------------------------------------- diff --git a/test/ThreadPoolDequeTest.cpp b/test/ThreadPoolDequeTest.cpp index c49ebe5..fa7f3c1 100644 --- a/test/ThreadPoolDequeTest.cpp +++ b/test/ThreadPoolDequeTest.cpp @@ -1,9 +1,20 @@ #include #include #include +#include #include #include "ThreadPool.hpp" +TEST(ThreadModeEnumTest, NamesMatchExpectedValues) { + EXPECT_EQ(ThreadPool::ThreadMode_name(ThreadPool::ThreadMode::STANDARD), "STANDARD"); + EXPECT_EQ(ThreadPool::ThreadMode_name(ThreadPool::ThreadMode::PRIORITY), "PRIORITY"); +} + +TEST(ThreadSynchronizationEnumTest, NamesMatchExpectedValues) { + EXPECT_EQ(ThreadPool::ThreadSynchronization_name(ThreadPool::ThreadSynchronization::ASYNCHRONOUS), "ASYNCHRONOUS"); + EXPECT_EQ(ThreadPool::ThreadSynchronization_name(ThreadPool::ThreadSynchronization::SYNCHRONOUS), "SYNCHRONOUS"); +} + class ThreadPoolTest : public ::testing::Test { protected: void SetUp() override { @@ -168,6 +179,26 @@ TEST_F(ThreadPoolTest, HandleVaryingExecutionTimes) { } } +TEST_F(ThreadPoolTest, VoidTaskSynchronizesWhenRequested) { + std::atomic_int counter{0}; + + auto future = threadPool->queue([&counter] { counter++; }); + + ASSERT_TRUE(future.valid()); + future.wait(); + + EXPECT_EQ(counter.load(), 1); +} + +TEST_F(ThreadPoolTest, VoidTaskAsynchronousFireAndForget) { + std::atomic_int counter{0}; + for (int i = 0; i < 4; ++i) { + threadPool->queue([&counter] { counter.fetch_add(1, std::memory_order_relaxed); }); + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + EXPECT_EQ(counter.load(), 4); +} + TEST_F(ThreadPoolTest, AllThreadsRunningWithoutGet) { constexpr size_t taskCount = 1000; // Adjust as needed const size_t threadCount = std::thread::hardware_concurrency(); // Actual thread count @@ -201,4 +232,4 @@ TEST_F(ThreadPoolTest, AllThreadsRunningWithoutGet) { // int main(int argc, char **argv) { // ::testing::InitGoogleTest(&argc, argv); // return RUN_ALL_TESTS(); -// } \ No newline at end of file +// } diff --git a/test/ThreadPoolDisableDequeTest.cpp b/test/ThreadPoolDisableDequeTest.cpp index 141f508..709a263 100644 --- a/test/ThreadPoolDisableDequeTest.cpp +++ b/test/ThreadPoolDisableDequeTest.cpp @@ -1,9 +1,20 @@ #include #include #include +#include #include #include "ThreadPool.hpp" +TEST(ThreadModeEnumTest, NamesMatchExpectedValues) { + EXPECT_EQ(ThreadPool::ThreadMode_name(ThreadPool::ThreadMode::STANDARD), "STANDARD"); + EXPECT_EQ(ThreadPool::ThreadMode_name(ThreadPool::ThreadMode::PRIORITY), "PRIORITY"); +} + +TEST(ThreadSynchronizationEnumTest, NamesMatchExpectedValues) { + EXPECT_EQ(ThreadPool::ThreadSynchronization_name(ThreadPool::ThreadSynchronization::ASYNCHRONOUS), "ASYNCHRONOUS"); + EXPECT_EQ(ThreadPool::ThreadSynchronization_name(ThreadPool::ThreadSynchronization::SYNCHRONOUS), "SYNCHRONOUS"); +} + class ThreadPoolTest : public ::testing::Test { protected: void SetUp() override { @@ -168,6 +179,26 @@ TEST_F(ThreadPoolTest, HandleVaryingExecutionTimes) { } } +TEST_F(ThreadPoolTest, VoidTaskSynchronizesWhenRequested) { + std::atomic_int counter{0}; + + auto future = threadPool->queue([&counter] { counter++; }); + + ASSERT_TRUE(future.valid()); + future.wait(); + + EXPECT_EQ(counter.load(), 1); +} + +TEST_F(ThreadPoolTest, VoidTaskAsynchronousFireAndForget) { + std::atomic_int counter{0}; + for (int i = 0; i < 4; ++i) { + threadPool->queue([&counter] { counter.fetch_add(1, std::memory_order_relaxed); }); + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + EXPECT_EQ(counter.load(), 4); +} + TEST_F(ThreadPoolTest, AllThreadsRunningWithoutGet) { constexpr size_t taskCount = 1000; // Adjust as needed const size_t threadCount = std::thread::hardware_concurrency(); // Actual thread count @@ -201,4 +232,4 @@ TEST_F(ThreadPoolTest, AllThreadsRunningWithoutGet) { // int main(int argc, char **argv) { // ::testing::InitGoogleTest(&argc, argv); // return RUN_ALL_TESTS(); -// } \ No newline at end of file +// } diff --git a/test/ThreadPoolDisableTest.cpp b/test/ThreadPoolDisableTest.cpp index e9994f0..025bc8c 100644 --- a/test/ThreadPoolDisableTest.cpp +++ b/test/ThreadPoolDisableTest.cpp @@ -1,9 +1,20 @@ #include #include #include +#include #include #include "ThreadPool.hpp" +TEST(ThreadModeEnumTest, NamesMatchExpectedValues) { + EXPECT_EQ(ThreadPool::ThreadMode_name(ThreadPool::ThreadMode::STANDARD), "STANDARD"); + EXPECT_EQ(ThreadPool::ThreadMode_name(ThreadPool::ThreadMode::PRIORITY), "PRIORITY"); +} + +TEST(ThreadSynchronizationEnumTest, NamesMatchExpectedValues) { + EXPECT_EQ(ThreadPool::ThreadSynchronization_name(ThreadPool::ThreadSynchronization::ASYNCHRONOUS), "ASYNCHRONOUS"); + EXPECT_EQ(ThreadPool::ThreadSynchronization_name(ThreadPool::ThreadSynchronization::SYNCHRONOUS), "SYNCHRONOUS"); +} + class ThreadPoolTest : public ::testing::Test { protected: void SetUp() override { @@ -171,6 +182,27 @@ TEST_F(ThreadPoolTest, HandleVaryingExecutionTimes) { } } +TEST_F(ThreadPoolTest, VoidTaskCanBeSynchronizedWhenRequested) { + std::atomic_int counter{0}; + + auto taskBuilder = threadPool->queue(true, [&counter] { counter++; }); + auto future = taskBuilder.get_future(); + + ASSERT_TRUE(future.valid()); + future.wait(); + + EXPECT_EQ(counter.load(), 1); +} + +TEST_F(ThreadPoolTest, VoidTaskAsynchronousFireAndForget) { + std::atomic_int counter{0}; + for (int i = 0; i < 4; ++i) { + threadPool->queue(true, [&counter] { counter.fetch_add(1, std::memory_order_relaxed); }); + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + EXPECT_EQ(counter.load(), 4); +} + TEST_F(ThreadPoolTest, AllThreadsRunningWithoutGet) { constexpr size_t taskCount = 1000; // Adjust as needed const size_t threadCount = std::thread::hardware_concurrency(); // Actual thread count diff --git a/test/ThreadPoolTest.cpp b/test/ThreadPoolTest.cpp index 564df57..480026e 100644 --- a/test/ThreadPoolTest.cpp +++ b/test/ThreadPoolTest.cpp @@ -1,9 +1,20 @@ #include #include #include +#include #include #include "ThreadPool.hpp" +TEST(ThreadModeEnumTest, NamesMatchExpectedValues) { + EXPECT_EQ(ThreadPool::ThreadMode_name(ThreadPool::ThreadMode::STANDARD), "STANDARD"); + EXPECT_EQ(ThreadPool::ThreadMode_name(ThreadPool::ThreadMode::PRIORITY), "PRIORITY"); +} + +TEST(ThreadSynchronizationEnumTest, NamesMatchExpectedValues) { + EXPECT_EQ(ThreadPool::ThreadSynchronization_name(ThreadPool::ThreadSynchronization::ASYNCHRONOUS), "ASYNCHRONOUS"); + EXPECT_EQ(ThreadPool::ThreadSynchronization_name(ThreadPool::ThreadSynchronization::SYNCHRONOUS), "SYNCHRONOUS"); +} + class ThreadPoolTest : public ::testing::Test { protected: void SetUp() override { @@ -171,6 +182,27 @@ TEST_F(ThreadPoolTest, HandleVaryingExecutionTimes) { } } +TEST_F(ThreadPoolTest, VoidTaskCanBeSynchronizedWhenRequested) { + std::atomic_int counter{0}; + + auto taskBuilder = threadPool->queue(true, [&counter] { counter++; }); + auto future = taskBuilder.get_future(); + + ASSERT_TRUE(future.valid()); + future.wait(); + + EXPECT_EQ(counter.load(), 1); +} + +TEST_F(ThreadPoolTest, VoidTaskAsynchronousFireAndForget) { + std::atomic_int counter{0}; + for (int i = 0; i < 4; ++i) { + threadPool->queue(true, [&counter] { counter.fetch_add(1, std::memory_order_relaxed); }); + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + EXPECT_EQ(counter.load(), 4); +} + TEST_F(ThreadPoolTest, AllThreadsRunningWithoutGet) { constexpr size_t taskCount = 1000; // Adjust as needed const size_t threadCount = std::thread::hardware_concurrency(); // Actual thread count