From 1a4eaba4318c50c929716ea4ed5a3e4bcd211648 Mon Sep 17 00:00:00 2001 From: Guo Date: Wed, 22 Jul 2026 09:04:50 -0500 Subject: [PATCH 01/24] add thread_pool component --- components/thread_pool/CMakeLists.txt | 4 + components/thread_pool/README.md | 13 ++ components/thread_pool/example/CMakeLists.txt | 22 +++ components/thread_pool/example/README.md | 17 ++ .../thread_pool/example/main/CMakeLists.txt | 2 + .../example/main/thread_pool_example.cpp | 67 ++++++++ .../thread_pool/example/sdkconfig.defaults | 1 + components/thread_pool/idf_component.yml | 21 +++ .../thread_pool/include/thread_pool.hpp | 81 ++++++++++ components/thread_pool/src/thread_pool.cpp | 150 ++++++++++++++++++ 10 files changed, 378 insertions(+) create mode 100644 components/thread_pool/CMakeLists.txt create mode 100644 components/thread_pool/README.md create mode 100644 components/thread_pool/example/CMakeLists.txt create mode 100644 components/thread_pool/example/README.md create mode 100644 components/thread_pool/example/main/CMakeLists.txt create mode 100644 components/thread_pool/example/main/thread_pool_example.cpp create mode 100644 components/thread_pool/example/sdkconfig.defaults create mode 100644 components/thread_pool/idf_component.yml create mode 100644 components/thread_pool/include/thread_pool.hpp create mode 100644 components/thread_pool/src/thread_pool.cpp diff --git a/components/thread_pool/CMakeLists.txt b/components/thread_pool/CMakeLists.txt new file mode 100644 index 000000000..cb1e7ccab --- /dev/null +++ b/components/thread_pool/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register( + INCLUDE_DIRS "include" + SRC_DIRS "src" + REQUIRES base_component task) diff --git a/components/thread_pool/README.md b/components/thread_pool/README.md new file mode 100644 index 000000000..271e12ce7 --- /dev/null +++ b/components/thread_pool/README.md @@ -0,0 +1,13 @@ +# Thread Pool Component + +The `ThreadPool` component provides a reusable pool of worker tasks for executing queued jobs asynchronously. + +It is implemented with `espp::Task` workers and `std::condition_variable` synchronization. + +## Features + +- Configurable worker count +- Bounded or unbounded queue +- Optional blocking submit mode for backpressure +- Graceful stop (drains queued jobs) +- Thread-safe stats for submitted / executed / rejected jobs diff --git a/components/thread_pool/example/CMakeLists.txt b/components/thread_pool/example/CMakeLists.txt new file mode 100644 index 000000000..d480eb92c --- /dev/null +++ b/components/thread_pool/example/CMakeLists.txt @@ -0,0 +1,22 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.20) + +set(ENV{IDF_COMPONENT_MANAGER} "0") +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +# add the component directories that we want to use +set(EXTRA_COMPONENT_DIRS + "../../../components/" +) + +set( + COMPONENTS + "main esptool_py thread_pool" + CACHE STRING + "List of components to include" + ) + +project(thread_pool_example) + +set(CMAKE_CXX_STANDARD 20) diff --git a/components/thread_pool/example/README.md b/components/thread_pool/example/README.md new file mode 100644 index 000000000..4c0790c06 --- /dev/null +++ b/components/thread_pool/example/README.md @@ -0,0 +1,17 @@ +# Thread Pool Example + +This example shows how to: + +- Create an `espp::ThreadPool` +- Submit jobs +- Wait for all jobs to complete +- Read execution statistics + +## Build + +From this directory, run: + +```bash +idf.py set-target esp32 +idf.py build flash monitor +``` diff --git a/components/thread_pool/example/main/CMakeLists.txt b/components/thread_pool/example/main/CMakeLists.txt new file mode 100644 index 000000000..a941e22ba --- /dev/null +++ b/components/thread_pool/example/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRC_DIRS "." + INCLUDE_DIRS ".") diff --git a/components/thread_pool/example/main/thread_pool_example.cpp b/components/thread_pool/example/main/thread_pool_example.cpp new file mode 100644 index 000000000..4191e5b25 --- /dev/null +++ b/components/thread_pool/example/main/thread_pool_example.cpp @@ -0,0 +1,67 @@ +#include +#include +#include +#include +#include + +#include "logger.hpp" +#include "thread_pool.hpp" + +using namespace std::chrono_literals; + +extern "C" void app_main(void) { + espp::Logger logger({.tag = "ThreadPool Example", .level = espp::Logger::Verbosity::INFO}); + + std::mutex done_mutex; + std::condition_variable done_cv; + constexpr int total_jobs = 8; + std::atomic completed_jobs = 0; + + espp::ThreadPool pool({ + .worker_count = 2, + .max_queue_size = 16, + .auto_start = true, + .block_on_submit_when_full = false, + .worker_task_config = { + .name = "tp_worker", + .stack_size_bytes = 4096, + .priority = 5, + .core_id = -1, + }, + .log_level = espp::Logger::Verbosity::WARN, + }); + + logger.info("Submitting {} jobs", total_jobs); + + for (int i = 0; i < total_jobs; ++i) { + bool queued = pool.submit([&, i]() { + std::this_thread::sleep_for(100ms); + int done = ++completed_jobs; + logger.info("Job {} done ({}/{})", i, done, total_jobs); + if (done == total_jobs) { + std::lock_guard lock(done_mutex); + done_cv.notify_one(); + } + }); + + if (!queued) { + logger.error("Failed to queue job {}", i); + } + } + + { + std::unique_lock lock(done_mutex); + done_cv.wait(lock, [&]() { return completed_jobs.load() == total_jobs; }); + } + + auto stats = pool.stats(); + logger.info("ThreadPool stats: submitted={} executed={} rejected={}", stats.submitted, + stats.executed, stats.rejected); + + pool.stop(); + logger.info("ThreadPool example complete"); + + while (true) { + std::this_thread::sleep_for(1s); + } +} diff --git a/components/thread_pool/example/sdkconfig.defaults b/components/thread_pool/example/sdkconfig.defaults new file mode 100644 index 000000000..75ebeae1c --- /dev/null +++ b/components/thread_pool/example/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_COMPILER_CXX_EXCEPTIONS=y diff --git a/components/thread_pool/idf_component.yml b/components/thread_pool/idf_component.yml new file mode 100644 index 000000000..71a535f71 --- /dev/null +++ b/components/thread_pool/idf_component.yml @@ -0,0 +1,21 @@ +## IDF Component Manager Manifest File +license: "MIT" +description: "Generic thread pool component implemented with espp::Task and condition variables." +url: "https://github.com/esp-cpp/espp/tree/main/components/thread_pool" +repository: "git://github.com/esp-cpp/espp.git" +maintainers: + - William Emfinger +documentation: "https://esp-cpp.github.io/espp/core/thread_pool.html" +examples: + - path: example +tags: + - cpp + - Component + - ThreadPool + - Task + - Concurrent +dependencies: + idf: + version: '>=5.0' + espp/base_component: '>=1.0' + espp/task: '>=1.0' diff --git a/components/thread_pool/include/thread_pool.hpp b/components/thread_pool/include/thread_pool.hpp new file mode 100644 index 000000000..b2f903c57 --- /dev/null +++ b/components/thread_pool/include/thread_pool.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "base_component.hpp" +#include "task.hpp" + +namespace espp { + +class ThreadPool : public espp::BaseComponent { +public: + using Job = std::function; + + struct Stats { + std::uint64_t submitted = 0; + std::uint64_t executed = 0; + std::uint64_t rejected = 0; + }; + + struct Config { + std::size_t worker_count = 1; + std::size_t max_queue_size = 0; // 0 means unbounded + bool auto_start = true; + bool block_on_submit_when_full = false; + espp::Task::BaseConfig worker_task_config = { + .name = "thread_pool_worker", + .stack_size_bytes = 4096, + .priority = 5, + .core_id = -1, + }; + espp::Logger::Verbosity log_level = espp::Logger::Verbosity::WARN; + }; + + explicit ThreadPool(const Config &config); + ~ThreadPool(); + + void start(); + void stop(); + + bool is_running() const; + + bool submit(const Job &job); + bool try_submit(const Job &job); + + std::size_t queue_size() const; + std::size_t worker_count() const; + + Stats stats() const; + +private: + bool worker_task_fn(std::mutex &task_mutex, std::condition_variable &task_cv, + bool &task_notified); + + bool submit_impl(const Job &job, bool allow_blocking_when_full); + + Config config_; + + mutable std::mutex queue_mutex_; + std::condition_variable queue_has_work_cv_; + std::condition_variable queue_has_space_cv_; + std::deque queue_; + + std::vector> workers_; + std::atomic running_{false}; + bool stopping_ = false; + + std::atomic submitted_{0}; + std::atomic executed_{0}; + std::atomic rejected_{0}; +}; + +} // namespace espp diff --git a/components/thread_pool/src/thread_pool.cpp b/components/thread_pool/src/thread_pool.cpp new file mode 100644 index 000000000..55b6d2faa --- /dev/null +++ b/components/thread_pool/src/thread_pool.cpp @@ -0,0 +1,150 @@ +#include "thread_pool.hpp" + +#include + +using namespace espp; + +ThreadPool::ThreadPool(const Config &config) + : BaseComponent("ThreadPool", config.log_level), config_(config) { + if (config_.worker_count == 0) { + config_.worker_count = 1; + } + + workers_.reserve(config_.worker_count); + for (std::size_t i = 0; i < config_.worker_count; ++i) { + auto worker_config = config_.worker_task_config; + worker_config.name = config_.worker_task_config.name + "_" + std::to_string(i); + using namespace std::placeholders; + workers_.emplace_back(espp::Task::make_unique({ + .callback = std::bind(&ThreadPool::worker_task_fn, this, _1, _2, _3), + .task_config = worker_config, + .log_level = config_.log_level, + })); + } + + if (config_.auto_start) { + start(); + } +} + +ThreadPool::~ThreadPool() { stop(); } + +void ThreadPool::start() { + if (running_.exchange(true)) { + return; + } + + { + std::lock_guard lock(queue_mutex_); + stopping_ = false; + } + + for (auto &worker : workers_) { + worker->start(); + } +} + +void ThreadPool::stop() { + if (!running_.exchange(false)) { + return; + } + + { + std::lock_guard lock(queue_mutex_); + stopping_ = true; + } + + queue_has_work_cv_.notify_all(); + queue_has_space_cv_.notify_all(); + + for (auto &worker : workers_) { + worker->stop(); + } +} + +bool ThreadPool::is_running() const { return running_.load(); } + +bool ThreadPool::submit(const Job &job) { + return submit_impl(job, config_.block_on_submit_when_full); +} + +bool ThreadPool::try_submit(const Job &job) { return submit_impl(job, false); } + +bool ThreadPool::submit_impl(const Job &job, bool allow_blocking_when_full) { + if (!job) { + rejected_++; + return false; + } + + std::unique_lock lock(queue_mutex_); + if (!running_.load() || stopping_) { + rejected_++; + return false; + } + + if (config_.max_queue_size > 0) { + if (allow_blocking_when_full) { + queue_has_space_cv_.wait(lock, [&]() { + return stopping_ || queue_.size() < config_.max_queue_size; + }); + if (stopping_) { + rejected_++; + return false; + } + } else if (queue_.size() >= config_.max_queue_size) { + rejected_++; + return false; + } + } + + queue_.push_back(job); + submitted_++; + lock.unlock(); + queue_has_work_cv_.notify_one(); + return true; +} + +std::size_t ThreadPool::queue_size() const { + std::lock_guard lock(queue_mutex_); + return queue_.size(); +} + +std::size_t ThreadPool::worker_count() const { return workers_.size(); } + +ThreadPool::Stats ThreadPool::stats() const { + return { + .submitted = submitted_.load(), + .executed = executed_.load(), + .rejected = rejected_.load(), + }; +} + +bool ThreadPool::worker_task_fn(std::mutex &task_mutex, + std::condition_variable &task_cv, + bool &task_notified) { + (void)task_cv; + + Job job; + { + std::unique_lock lock(queue_mutex_); + queue_has_work_cv_.wait(lock, [&]() { return stopping_ || !queue_.empty(); }); + + if (queue_.empty()) { + std::unique_lock task_lock(task_mutex); + return stopping_ || task_notified; + } + + job = std::move(queue_.front()); + queue_.pop_front(); + + if (config_.max_queue_size > 0) { + queue_has_space_cv_.notify_one(); + } + } + + job(); + executed_++; + + std::unique_lock task_lock(task_mutex); + return task_notified; +} From 9287962aff4494cf5a9fdd15f55cf3600cbeab04 Mon Sep 17 00:00:00 2001 From: Guo Date: Wed, 22 Jul 2026 09:09:53 -0500 Subject: [PATCH 02/24] add comments to all the public functions --- .../thread_pool/include/thread_pool.hpp | 57 ++++++++++++++++--- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/components/thread_pool/include/thread_pool.hpp b/components/thread_pool/include/thread_pool.hpp index b2f903c57..1ca80eb23 100644 --- a/components/thread_pool/include/thread_pool.hpp +++ b/components/thread_pool/include/thread_pool.hpp @@ -16,44 +16,83 @@ namespace espp { +/// @brief A thread pool that dispatches submitted jobs to a fixed set of worker threads. +/// +/// Workers are implemented as espp::Task instances. Jobs are queued and +/// consumed in FIFO order. The queue can be optionally bounded; when full, +/// new submissions are either rejected immediately or blocked until space +/// becomes available, depending on the configuration. class ThreadPool : public espp::BaseComponent { public: + /// @brief A callable job that can be submitted to the pool. using Job = std::function; + /// @brief Snapshot of pool activity counters. struct Stats { - std::uint64_t submitted = 0; - std::uint64_t executed = 0; - std::uint64_t rejected = 0; + std::uint64_t submitted = 0; ///< Total jobs ever submitted (including rejected). + std::uint64_t executed = 0; ///< Total jobs successfully executed. + std::uint64_t rejected = 0; ///< Total jobs rejected because the queue was full. }; + /// @brief Configuration parameters for constructing a ThreadPool. struct Config { - std::size_t worker_count = 1; - std::size_t max_queue_size = 0; // 0 means unbounded - bool auto_start = true; - bool block_on_submit_when_full = false; - espp::Task::BaseConfig worker_task_config = { + std::size_t worker_count = 1; ///< Number of worker threads to spawn. + std::size_t max_queue_size = 0; ///< Maximum pending jobs (0 = unbounded). + bool auto_start = true; ///< Start workers immediately on construction. + bool block_on_submit_when_full = false; ///< If true, submit() blocks when the queue is full instead of rejecting. + espp::Task::BaseConfig worker_task_config = { ///< Base configuration applied to every worker task. .name = "thread_pool_worker", .stack_size_bytes = 4096, .priority = 5, .core_id = -1, }; - espp::Logger::Verbosity log_level = espp::Logger::Verbosity::WARN; + espp::Logger::Verbosity log_level = espp::Logger::Verbosity::WARN; ///< Logger verbosity level. }; + /// @brief Construct the pool with the given configuration. + /// @param config Pool configuration. explicit ThreadPool(const Config &config); + + /// @brief Destroy the pool, stopping all workers gracefully. ~ThreadPool(); + /// @brief Start all worker threads. + /// @note No-op if the pool is already running. void start(); + + /// @brief Stop all worker threads and drain the queue. + /// @note Blocks until every worker has exited. void stop(); + /// @brief Query whether the pool is currently running. + /// @return true if workers are active, false otherwise. bool is_running() const; + /// @brief Submit a job, optionally blocking when the queue is full. + /// + /// Blocks if Config::block_on_submit_when_full is true and the queue has + /// reached its capacity limit. Otherwise behaves identically to try_submit(). + /// @param job Callable to enqueue. + /// @return true if the job was accepted, false if it was rejected. bool submit(const Job &job); + + /// @brief Attempt to submit a job without blocking. + /// + /// Returns immediately with false when the queue is full. + /// @param job Callable to enqueue. + /// @return true if the job was accepted, false if it was rejected. bool try_submit(const Job &job); + /// @brief Return the number of jobs currently waiting in the queue. + /// @return Pending job count. std::size_t queue_size() const; + + /// @brief Return the number of worker threads in the pool. + /// @return Worker thread count. std::size_t worker_count() const; + /// @brief Return a snapshot of the pool's activity counters. + /// @return Stats struct with submitted, executed, and rejected counts. Stats stats() const; private: From d0a79ccf9dd55f9677b67bef4a0a5f15ddda33eb Mon Sep 17 00:00:00 2001 From: guo-max Date: Wed, 22 Jul 2026 10:18:08 -0500 Subject: [PATCH 03/24] unlock queue mutex when queue is empty. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- components/thread_pool/src/thread_pool.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/thread_pool/src/thread_pool.cpp b/components/thread_pool/src/thread_pool.cpp index 55b6d2faa..d949fe444 100644 --- a/components/thread_pool/src/thread_pool.cpp +++ b/components/thread_pool/src/thread_pool.cpp @@ -130,8 +130,10 @@ bool ThreadPool::worker_task_fn(std::mutex &task_mutex, queue_has_work_cv_.wait(lock, [&]() { return stopping_ || !queue_.empty(); }); if (queue_.empty()) { + bool stopping = stopping_; + lock.unlock(); std::unique_lock task_lock(task_mutex); - return stopping_ || task_notified; + return stopping || task_notified; } job = std::move(queue_.front()); From ce96a418440952a7dcffb128162b7848b1beb28b Mon Sep 17 00:00:00 2001 From: guo-max Date: Wed, 22 Jul 2026 10:19:30 -0500 Subject: [PATCH 04/24] fix status comments Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- components/thread_pool/include/thread_pool.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/thread_pool/include/thread_pool.hpp b/components/thread_pool/include/thread_pool.hpp index 1ca80eb23..ab6bb610b 100644 --- a/components/thread_pool/include/thread_pool.hpp +++ b/components/thread_pool/include/thread_pool.hpp @@ -29,9 +29,9 @@ class ThreadPool : public espp::BaseComponent { /// @brief Snapshot of pool activity counters. struct Stats { - std::uint64_t submitted = 0; ///< Total jobs ever submitted (including rejected). + std::uint64_t submitted = 0; ///< Total jobs accepted into the queue. std::uint64_t executed = 0; ///< Total jobs successfully executed. - std::uint64_t rejected = 0; ///< Total jobs rejected because the queue was full. + std::uint64_t rejected = 0; ///< Total jobs rejected (invalid job, stopped/stopping, or queue full). }; /// @brief Configuration parameters for constructing a ThreadPool. From 53c150b1e614202af64d606dccb8623559fb7e77 Mon Sep 17 00:00:00 2001 From: guo-max Date: Wed, 22 Jul 2026 10:19:48 -0500 Subject: [PATCH 05/24] fix stop function comments Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- components/thread_pool/include/thread_pool.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/thread_pool/include/thread_pool.hpp b/components/thread_pool/include/thread_pool.hpp index ab6bb610b..6e31529e7 100644 --- a/components/thread_pool/include/thread_pool.hpp +++ b/components/thread_pool/include/thread_pool.hpp @@ -60,8 +60,8 @@ class ThreadPool : public espp::BaseComponent { /// @note No-op if the pool is already running. void start(); - /// @brief Stop all worker threads and drain the queue. - /// @note Blocks until every worker has exited. + /// @brief Stop all worker threads and reject further submissions. + /// @note Blocks until every worker has exited; queued jobs may not be executed. void stop(); /// @brief Query whether the pool is currently running. From 98d922391beb995d58a856263fa725320e375fdb Mon Sep 17 00:00:00 2001 From: guo-max Date: Wed, 22 Jul 2026 10:25:34 -0500 Subject: [PATCH 06/24] fix readme Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- components/thread_pool/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/thread_pool/README.md b/components/thread_pool/README.md index 271e12ce7..1ffe3cbc9 100644 --- a/components/thread_pool/README.md +++ b/components/thread_pool/README.md @@ -9,5 +9,5 @@ It is implemented with `espp::Task` workers and `std::condition_variable` synchr - Configurable worker count - Bounded or unbounded queue - Optional blocking submit mode for backpressure -- Graceful stop (drains queued jobs) +- Graceful stop (stops workers; queued jobs may not be executed) - Thread-safe stats for submitted / executed / rejected jobs From 5da2ee13d6613c87d79500ca35c31a0c07128f7a Mon Sep 17 00:00:00 2001 From: Guo Date: Wed, 22 Jul 2026 10:28:19 -0500 Subject: [PATCH 07/24] add log if task not started --- components/thread_pool/src/thread_pool.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/components/thread_pool/src/thread_pool.cpp b/components/thread_pool/src/thread_pool.cpp index d949fe444..c519e56e9 100644 --- a/components/thread_pool/src/thread_pool.cpp +++ b/components/thread_pool/src/thread_pool.cpp @@ -39,8 +39,13 @@ void ThreadPool::start() { stopping_ = false; } - for (auto &worker : workers_) { - worker->start(); + for (std::size_t i = 0; i < workers_.size(); ++i) { + auto &worker = workers_[i]; + if (!worker->start()) { + // Handle the error if needed, e.g., log it + logger_.warn("Failed to start worker thread {}. could be already started or not enough memory", i); + } + } } @@ -130,10 +135,8 @@ bool ThreadPool::worker_task_fn(std::mutex &task_mutex, queue_has_work_cv_.wait(lock, [&]() { return stopping_ || !queue_.empty(); }); if (queue_.empty()) { - bool stopping = stopping_; - lock.unlock(); std::unique_lock task_lock(task_mutex); - return stopping || task_notified; + return stopping_ || task_notified; } job = std::move(queue_.front()); From fa1852ec34f61f1863e4e771474fad9b3fe0e59b Mon Sep 17 00:00:00 2001 From: Guo Date: Wed, 22 Jul 2026 10:53:09 -0500 Subject: [PATCH 08/24] udpate documentation --- .github/workflows/build.yml | 2 ++ .github/workflows/upload_components.yml | 1 + doc/en/core/index.rst | 1 + doc/en/core/thread_pool.rst | 27 +++++++++++++++++++++++++ doc/en/core/thread_pool_example.md | 2 ++ 5 files changed, 33 insertions(+) create mode 100644 doc/en/core/thread_pool.rst create mode 100644 doc/en/core/thread_pool_example.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 496dd420d..8756e7869 100755 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -229,6 +229,8 @@ jobs: target: esp32 - path: 'components/task/example' target: esp32 + - path: 'components/thread_pool/example' + target: esp32 - path: 'components/thermistor/example' target: esp32 - path: 'components/timer/example' diff --git a/.github/workflows/upload_components.yml b/.github/workflows/upload_components.yml index f2f749d35..f4c072905 100755 --- a/.github/workflows/upload_components.yml +++ b/.github/workflows/upload_components.yml @@ -133,6 +133,7 @@ jobs: components/t-dongle-s3 components/tabulate components/task + components/thread_pool components/thermistor components/timer components/touch diff --git a/doc/en/core/index.rst b/doc/en/core/index.rst index 07ec25562..4e7f07a0d 100644 --- a/doc/en/core/index.rst +++ b/doc/en/core/index.rst @@ -11,6 +11,7 @@ the command-line interface. base_component base_peripheral task + thread_pool runqueue timer interrupt diff --git a/doc/en/core/thread_pool.rst b/doc/en/core/thread_pool.rst new file mode 100644 index 000000000..950e93d4d --- /dev/null +++ b/doc/en/core/thread_pool.rst @@ -0,0 +1,27 @@ +Thread Pool APIs +**************** + +ThreadPool +---------- + +The `ThreadPool` component provides a reusable pool of worker tasks for +executing queued jobs asynchronously. Workers are implemented as +:cpp:class:`espp::Task` instances and communicate through an optionally bounded +:cpp:class:`std::deque`. Submissions can either reject immediately when the +queue is full or block until space is available, depending on configuration. + +Code examples for the thread pool API are provided in the `thread_pool` example +folder. + +.. ------------------------------- Example ------------------------------------- + +.. toctree:: + + thread_pool_example + +.. ---------------------------- API Reference ---------------------------------- + +API Reference +------------- + +.. include-build-file:: inc/thread_pool.inc diff --git a/doc/en/core/thread_pool_example.md b/doc/en/core/thread_pool_example.md new file mode 100644 index 000000000..c97ae4e96 --- /dev/null +++ b/doc/en/core/thread_pool_example.md @@ -0,0 +1,2 @@ +```{include} ../../components/thread_pool/example/README.md +``` From 9df0de9d10961b4cf7d4750d8b589e2d1dbcafc1 Mon Sep 17 00:00:00 2001 From: guo-max Date: Wed, 22 Jul 2026 11:23:41 -0500 Subject: [PATCH 09/24] fix mutex with task_notified. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- components/thread_pool/src/thread_pool.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/components/thread_pool/src/thread_pool.cpp b/components/thread_pool/src/thread_pool.cpp index c519e56e9..74652820e 100644 --- a/components/thread_pool/src/thread_pool.cpp +++ b/components/thread_pool/src/thread_pool.cpp @@ -150,6 +150,8 @@ bool ThreadPool::worker_task_fn(std::mutex &task_mutex, job(); executed_++; - std::unique_lock task_lock(task_mutex); - return task_notified; + std::lock_guard task_lock(task_mutex); + const bool stop_requested = task_notified; + task_notified = false; + return stop_requested; } From 929b02f6930ab8f2e488b363269aa5f70d1da7e7 Mon Sep 17 00:00:00 2001 From: Guo Date: Wed, 22 Jul 2026 11:51:45 -0500 Subject: [PATCH 10/24] use std::move instead of copy for job submit --- components/thread_pool/include/thread_pool.hpp | 10 +++++----- components/thread_pool/src/thread_pool.cpp | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/components/thread_pool/include/thread_pool.hpp b/components/thread_pool/include/thread_pool.hpp index 6e31529e7..4140dba7f 100644 --- a/components/thread_pool/include/thread_pool.hpp +++ b/components/thread_pool/include/thread_pool.hpp @@ -72,16 +72,16 @@ class ThreadPool : public espp::BaseComponent { /// /// Blocks if Config::block_on_submit_when_full is true and the queue has /// reached its capacity limit. Otherwise behaves identically to try_submit(). - /// @param job Callable to enqueue. + /// @param job Callable to enqueue; moved into the queue on acceptance. /// @return true if the job was accepted, false if it was rejected. - bool submit(const Job &job); + bool submit(Job job); /// @brief Attempt to submit a job without blocking. /// /// Returns immediately with false when the queue is full. - /// @param job Callable to enqueue. + /// @param job Callable to enqueue; moved into the queue on acceptance. /// @return true if the job was accepted, false if it was rejected. - bool try_submit(const Job &job); + bool try_submit(Job job); /// @brief Return the number of jobs currently waiting in the queue. /// @return Pending job count. @@ -99,7 +99,7 @@ class ThreadPool : public espp::BaseComponent { bool worker_task_fn(std::mutex &task_mutex, std::condition_variable &task_cv, bool &task_notified); - bool submit_impl(const Job &job, bool allow_blocking_when_full); + bool submit_impl(Job job, bool allow_blocking_when_full); Config config_; diff --git a/components/thread_pool/src/thread_pool.cpp b/components/thread_pool/src/thread_pool.cpp index 74652820e..f16d7f3f9 100644 --- a/components/thread_pool/src/thread_pool.cpp +++ b/components/thread_pool/src/thread_pool.cpp @@ -69,13 +69,13 @@ void ThreadPool::stop() { bool ThreadPool::is_running() const { return running_.load(); } -bool ThreadPool::submit(const Job &job) { - return submit_impl(job, config_.block_on_submit_when_full); +bool ThreadPool::submit(Job job) { + return submit_impl(std::move(job), config_.block_on_submit_when_full); } -bool ThreadPool::try_submit(const Job &job) { return submit_impl(job, false); } +bool ThreadPool::try_submit(Job job) { return submit_impl(std::move(job), false); } -bool ThreadPool::submit_impl(const Job &job, bool allow_blocking_when_full) { +bool ThreadPool::submit_impl(Job job, bool allow_blocking_when_full) { if (!job) { rejected_++; return false; @@ -102,7 +102,7 @@ bool ThreadPool::submit_impl(const Job &job, bool allow_blocking_when_full) { } } - queue_.push_back(job); + queue_.push_back(std::move(job)); submitted_++; lock.unlock(); queue_has_work_cv_.notify_one(); From 864bb8541e09dcd1ac2582376a5cf0fe732d15a9 Mon Sep 17 00:00:00 2001 From: Guo Date: Wed, 22 Jul 2026 11:55:04 -0500 Subject: [PATCH 11/24] update example --- .../thread_pool/example/main/thread_pool_example.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/components/thread_pool/example/main/thread_pool_example.cpp b/components/thread_pool/example/main/thread_pool_example.cpp index 4191e5b25..339196a47 100644 --- a/components/thread_pool/example/main/thread_pool_example.cpp +++ b/components/thread_pool/example/main/thread_pool_example.cpp @@ -33,6 +33,7 @@ extern "C" void app_main(void) { logger.info("Submitting {} jobs", total_jobs); + int queued_jobs = 0; for (int i = 0; i < total_jobs; ++i) { bool queued = pool.submit([&, i]() { std::this_thread::sleep_for(100ms); @@ -46,12 +47,14 @@ extern "C" void app_main(void) { if (!queued) { logger.error("Failed to queue job {}", i); + } else { + ++queued_jobs; } } - { + if (queued_jobs > 0) { std::unique_lock lock(done_mutex); - done_cv.wait(lock, [&]() { return completed_jobs.load() == total_jobs; }); + done_cv.wait(lock, [&]() { return completed_jobs.load() == queued_jobs; }); } auto stats = pool.stats(); From 01138d596bbe1c55a747f64b6f4cb698186b9f89 Mon Sep 17 00:00:00 2001 From: Guo Date: Wed, 22 Jul 2026 13:17:01 -0500 Subject: [PATCH 12/24] revise per review --- .../example/main/thread_pool_example.cpp | 5 +-- components/thread_pool/src/thread_pool.cpp | 45 ++++++++++++++----- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/components/thread_pool/example/main/thread_pool_example.cpp b/components/thread_pool/example/main/thread_pool_example.cpp index 339196a47..3d26103c8 100644 --- a/components/thread_pool/example/main/thread_pool_example.cpp +++ b/components/thread_pool/example/main/thread_pool_example.cpp @@ -39,10 +39,7 @@ extern "C" void app_main(void) { std::this_thread::sleep_for(100ms); int done = ++completed_jobs; logger.info("Job {} done ({}/{})", i, done, total_jobs); - if (done == total_jobs) { - std::lock_guard lock(done_mutex); - done_cv.notify_one(); - } + done_cv.notify_one(); }); if (!queued) { diff --git a/components/thread_pool/src/thread_pool.cpp b/components/thread_pool/src/thread_pool.cpp index f16d7f3f9..9ab2ca9e4 100644 --- a/components/thread_pool/src/thread_pool.cpp +++ b/components/thread_pool/src/thread_pool.cpp @@ -30,22 +30,37 @@ ThreadPool::ThreadPool(const Config &config) ThreadPool::~ThreadPool() { stop(); } void ThreadPool::start() { - if (running_.exchange(true)) { - return; - } - { std::lock_guard lock(queue_mutex_); + if (running_.load()) { + return; + } stopping_ = false; + running_.store(true); } + std::size_t started_count = 0; for (std::size_t i = 0; i < workers_.size(); ++i) { - auto &worker = workers_[i]; - if (!worker->start()) { - // Handle the error if needed, e.g., log it - logger_.warn("Failed to start worker thread {}. could be already started or not enough memory", i); + if (workers_[i]->start()) { + ++started_count; + } else { + logger_.warn("Failed to start worker {} (already started or insufficient memory)", i); } + } + if (started_count == 0) { + logger_.error("No workers started; rolling back pool start"); + for (auto &worker : workers_) { + worker->stop(); + } + { + std::lock_guard lock(queue_mutex_); + running_.store(false); + stopping_ = true; + rejected_ += static_cast(queue_.size()); + queue_.clear(); + } + queue_has_space_cv_.notify_all(); } } @@ -57,6 +72,8 @@ void ThreadPool::stop() { { std::lock_guard lock(queue_mutex_); stopping_ = true; + rejected_ += static_cast(queue_.size()); + queue_.clear(); } queue_has_work_cv_.notify_all(); @@ -134,17 +151,21 @@ bool ThreadPool::worker_task_fn(std::mutex &task_mutex, std::unique_lock lock(queue_mutex_); queue_has_work_cv_.wait(lock, [&]() { return stopping_ || !queue_.empty(); }); + if (stopping_) { + return true; + } + if (queue_.empty()) { std::unique_lock task_lock(task_mutex); - return stopping_ || task_notified; + return task_notified; } job = std::move(queue_.front()); queue_.pop_front(); + } - if (config_.max_queue_size > 0) { - queue_has_space_cv_.notify_one(); - } + if (config_.max_queue_size > 0) { + queue_has_space_cv_.notify_one(); } job(); From fbe1f50de712c044b1396b70e4eb84a582bb1c97 Mon Sep 17 00:00:00 2001 From: Guo Date: Wed, 22 Jul 2026 13:59:37 -0500 Subject: [PATCH 13/24] update based on AI review --- components/thread_pool/include/thread_pool.hpp | 1 + components/thread_pool/src/thread_pool.cpp | 2 ++ doc/en/core/thread_pool.rst | 8 +++++--- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/components/thread_pool/include/thread_pool.hpp b/components/thread_pool/include/thread_pool.hpp index 4140dba7f..cdd2d49d8 100644 --- a/components/thread_pool/include/thread_pool.hpp +++ b/components/thread_pool/include/thread_pool.hpp @@ -103,6 +103,7 @@ class ThreadPool : public espp::BaseComponent { Config config_; + std::mutex lifecycle_mutex_; mutable std::mutex queue_mutex_; std::condition_variable queue_has_work_cv_; std::condition_variable queue_has_space_cv_; diff --git a/components/thread_pool/src/thread_pool.cpp b/components/thread_pool/src/thread_pool.cpp index 9ab2ca9e4..91f387131 100644 --- a/components/thread_pool/src/thread_pool.cpp +++ b/components/thread_pool/src/thread_pool.cpp @@ -30,6 +30,7 @@ ThreadPool::ThreadPool(const Config &config) ThreadPool::~ThreadPool() { stop(); } void ThreadPool::start() { + std::lock_guard lifecycle_lock(lifecycle_mutex_); { std::lock_guard lock(queue_mutex_); if (running_.load()) { @@ -65,6 +66,7 @@ void ThreadPool::start() { } void ThreadPool::stop() { + std::lock_guard lifecycle_lock(lifecycle_mutex_); if (!running_.exchange(false)) { return; } diff --git a/doc/en/core/thread_pool.rst b/doc/en/core/thread_pool.rst index 950e93d4d..2d1f0a675 100644 --- a/doc/en/core/thread_pool.rst +++ b/doc/en/core/thread_pool.rst @@ -6,9 +6,11 @@ ThreadPool The `ThreadPool` component provides a reusable pool of worker tasks for executing queued jobs asynchronously. Workers are implemented as -:cpp:class:`espp::Task` instances and communicate through an optionally bounded -:cpp:class:`std::deque`. Submissions can either reject immediately when the -queue is full or block until space is available, depending on configuration. +:cpp:class:`espp::Task` instances and pull work from an internal job queue +(backed by ``std::deque``) whose maximum size is optionally enforced by +:cpp:member:`espp::ThreadPool::Config::max_queue_size`. Submissions can either +reject immediately when the queue is full or block until space is available, +depending on configuration. Code examples for the thread pool API are provided in the `thread_pool` example folder. From 7c81329121f5253a484c92b07106ac5ab254f2a9 Mon Sep 17 00:00:00 2001 From: Guo Date: Wed, 22 Jul 2026 14:58:27 -0500 Subject: [PATCH 14/24] update based on review --- components/thread_pool/example/CMakeLists.txt | 4 ++-- .../thread_pool/include/thread_pool.hpp | 3 +-- components/thread_pool/src/thread_pool.cpp | 19 +++---------------- doc/en/core/thread_pool.rst | 4 ++-- 4 files changed, 8 insertions(+), 22 deletions(-) diff --git a/components/thread_pool/example/CMakeLists.txt b/components/thread_pool/example/CMakeLists.txt index d480eb92c..6019f3f82 100644 --- a/components/thread_pool/example/CMakeLists.txt +++ b/components/thread_pool/example/CMakeLists.txt @@ -2,6 +2,8 @@ # in this exact order for cmake to work correctly cmake_minimum_required(VERSION 3.20) +set(CMAKE_CXX_STANDARD 20) + set(ENV{IDF_COMPONENT_MANAGER} "0") include($ENV{IDF_PATH}/tools/cmake/project.cmake) @@ -18,5 +20,3 @@ set( ) project(thread_pool_example) - -set(CMAKE_CXX_STANDARD 20) diff --git a/components/thread_pool/include/thread_pool.hpp b/components/thread_pool/include/thread_pool.hpp index cdd2d49d8..b5b010d63 100644 --- a/components/thread_pool/include/thread_pool.hpp +++ b/components/thread_pool/include/thread_pool.hpp @@ -96,8 +96,7 @@ class ThreadPool : public espp::BaseComponent { Stats stats() const; private: - bool worker_task_fn(std::mutex &task_mutex, std::condition_variable &task_cv, - bool &task_notified); + bool worker_task_fn(); bool submit_impl(Job job, bool allow_blocking_when_full); diff --git a/components/thread_pool/src/thread_pool.cpp b/components/thread_pool/src/thread_pool.cpp index 91f387131..10279e02e 100644 --- a/components/thread_pool/src/thread_pool.cpp +++ b/components/thread_pool/src/thread_pool.cpp @@ -14,9 +14,8 @@ ThreadPool::ThreadPool(const Config &config) for (std::size_t i = 0; i < config_.worker_count; ++i) { auto worker_config = config_.worker_task_config; worker_config.name = config_.worker_task_config.name + "_" + std::to_string(i); - using namespace std::placeholders; workers_.emplace_back(espp::Task::make_unique({ - .callback = std::bind(&ThreadPool::worker_task_fn, this, _1, _2, _3), + .callback = [this]() { return worker_task_fn(); }, .task_config = worker_config, .log_level = config_.log_level, })); @@ -143,11 +142,7 @@ ThreadPool::Stats ThreadPool::stats() const { }; } -bool ThreadPool::worker_task_fn(std::mutex &task_mutex, - std::condition_variable &task_cv, - bool &task_notified) { - (void)task_cv; - +bool ThreadPool::worker_task_fn() { Job job; { std::unique_lock lock(queue_mutex_); @@ -157,11 +152,6 @@ bool ThreadPool::worker_task_fn(std::mutex &task_mutex, return true; } - if (queue_.empty()) { - std::unique_lock task_lock(task_mutex); - return task_notified; - } - job = std::move(queue_.front()); queue_.pop_front(); } @@ -173,8 +163,5 @@ bool ThreadPool::worker_task_fn(std::mutex &task_mutex, job(); executed_++; - std::lock_guard task_lock(task_mutex); - const bool stop_requested = task_notified; - task_notified = false; - return stop_requested; + return false; } diff --git a/doc/en/core/thread_pool.rst b/doc/en/core/thread_pool.rst index 2d1f0a675..f5ce0d47e 100644 --- a/doc/en/core/thread_pool.rst +++ b/doc/en/core/thread_pool.rst @@ -4,7 +4,7 @@ Thread Pool APIs ThreadPool ---------- -The `ThreadPool` component provides a reusable pool of worker tasks for +The :cpp:class:`espp::ThreadPool` component provides a reusable pool of worker tasks for executing queued jobs asynchronously. Workers are implemented as :cpp:class:`espp::Task` instances and pull work from an internal job queue (backed by ``std::deque``) whose maximum size is optionally enforced by @@ -12,7 +12,7 @@ executing queued jobs asynchronously. Workers are implemented as reject immediately when the queue is full or block until space is available, depending on configuration. -Code examples for the thread pool API are provided in the `thread_pool` example +Code examples for the thread pool API are provided in the ``thread_pool`` example folder. .. ------------------------------- Example ------------------------------------- From c734cf523eee4da8cfec4ef192779b88f1b90c00 Mon Sep 17 00:00:00 2001 From: Guo Date: Thu, 23 Jul 2026 09:49:05 -0500 Subject: [PATCH 15/24] add formatter for the stats struct --- .../example/main/thread_pool_example.cpp | 3 +-- components/thread_pool/include/thread_pool.hpp | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/components/thread_pool/example/main/thread_pool_example.cpp b/components/thread_pool/example/main/thread_pool_example.cpp index 3d26103c8..0396d275f 100644 --- a/components/thread_pool/example/main/thread_pool_example.cpp +++ b/components/thread_pool/example/main/thread_pool_example.cpp @@ -55,8 +55,7 @@ extern "C" void app_main(void) { } auto stats = pool.stats(); - logger.info("ThreadPool stats: submitted={} executed={} rejected={}", stats.submitted, - stats.executed, stats.rejected); + logger.info("ThreadPool stats: {}", stats); pool.stop(); logger.info("ThreadPool example complete"); diff --git a/components/thread_pool/include/thread_pool.hpp b/components/thread_pool/include/thread_pool.hpp index b5b010d63..f5f796ba7 100644 --- a/components/thread_pool/include/thread_pool.hpp +++ b/components/thread_pool/include/thread_pool.hpp @@ -118,3 +118,17 @@ class ThreadPool : public espp::BaseComponent { }; } // namespace espp + +// fmt formatter for espp::ThreadPool::Stats +template <> struct fmt::formatter { + template constexpr auto parse(ParseContext &ctx) const { + return ctx.begin(); + } + + template + auto format(espp::ThreadPool::Stats const &s, FormatContext &ctx) const { + return fmt::format_to(ctx.out(), + "ThreadPool::Stats{{submitted: {}, executed: {}, rejected: {}}}", + s.submitted, s.executed, s.rejected); + } +}; From 010b2f256b60c6686e512149c05e6cf49757e35f Mon Sep 17 00:00:00 2001 From: Guo Date: Thu, 23 Jul 2026 10:01:01 -0500 Subject: [PATCH 16/24] add more test for all the public functions --- .../example/main/thread_pool_example.cpp | 209 ++++++++++++++---- 1 file changed, 171 insertions(+), 38 deletions(-) diff --git a/components/thread_pool/example/main/thread_pool_example.cpp b/components/thread_pool/example/main/thread_pool_example.cpp index 0396d275f..5becc8938 100644 --- a/components/thread_pool/example/main/thread_pool_example.cpp +++ b/components/thread_pool/example/main/thread_pool_example.cpp @@ -9,55 +9,188 @@ using namespace std::chrono_literals; +static void wait_for_jobs(std::condition_variable &cv, std::mutex &mtx, std::atomic &completed, + int expected) { + std::unique_lock lock(mtx); + cv.wait(lock, [&]() { return completed.load() >= expected; }); +} + extern "C" void app_main(void) { espp::Logger logger({.tag = "ThreadPool Example", .level = espp::Logger::Verbosity::INFO}); - std::mutex done_mutex; - std::condition_variable done_cv; - constexpr int total_jobs = 8; - std::atomic completed_jobs = 0; - - espp::ThreadPool pool({ - .worker_count = 2, - .max_queue_size = 16, - .auto_start = true, - .block_on_submit_when_full = false, - .worker_task_config = { - .name = "tp_worker", - .stack_size_bytes = 4096, - .priority = 5, - .core_id = -1, - }, - .log_level = espp::Logger::Verbosity::WARN, - }); - - logger.info("Submitting {} jobs", total_jobs); - - int queued_jobs = 0; - for (int i = 0; i < total_jobs; ++i) { - bool queued = pool.submit([&, i]() { - std::this_thread::sleep_for(100ms); - int done = ++completed_jobs; - logger.info("Job {} done ({}/{})", i, done, total_jobs); - done_cv.notify_one(); + // ------------------------------------------------------------------------- + // 1. Manual start/stop + is_running() + worker_count() + // ------------------------------------------------------------------------- + logger.info("--- Test: auto_start=false, manual start() / stop() ---"); + { + espp::ThreadPool pool({ + .worker_count = 3, + .max_queue_size = 0, + .auto_start = false, + .worker_task_config = { + .name = "tp_worker", + .stack_size_bytes = 4096, + .priority = 5, + .core_id = -1, + }, + .log_level = espp::Logger::Verbosity::WARN, + }); + + logger.info("is_running before start: {}", pool.is_running()); // false + logger.info("worker_count: {}", pool.worker_count()); // 3 + + pool.start(); + logger.info("is_running after start: {}", pool.is_running()); // true + + // start() is a no-op when already running + pool.start(); + logger.info("is_running after second start: {}", pool.is_running()); // true + + pool.stop(); + logger.info("is_running after stop: {}", pool.is_running()); // false + } + + // ------------------------------------------------------------------------- + // 2. submit() — normal job dispatch, queue_size() + // ------------------------------------------------------------------------- + logger.info("--- Test: submit() and queue_size() ---"); + { + std::mutex done_mutex; + std::condition_variable done_cv; + constexpr int total_jobs = 8; + std::atomic completed_jobs = 0; + + espp::ThreadPool pool({ + .worker_count = 2, + .max_queue_size = 0, + .auto_start = true, + .worker_task_config = { + .name = "tp_worker", + .stack_size_bytes = 4096, + .priority = 5, + .core_id = -1, + }, + .log_level = espp::Logger::Verbosity::WARN, }); - if (!queued) { - logger.error("Failed to queue job {}", i); - } else { - ++queued_jobs; + for (int i = 0; i < total_jobs; ++i) { + bool accepted = pool.submit([&, i]() { + std::this_thread::sleep_for(50ms); + int done = ++completed_jobs; + logger.info("Job {} done ({}/{})", i, done, total_jobs); + done_cv.notify_one(); + }); + logger.info("submit job {}: accepted={}, queue_size={}", i, accepted, pool.queue_size()); } + + wait_for_jobs(done_cv, done_mutex, completed_jobs, total_jobs); + logger.info("Stats after submit test: {}", pool.stats()); + pool.stop(); } - if (queued_jobs > 0) { - std::unique_lock lock(done_mutex); - done_cv.wait(lock, [&]() { return completed_jobs.load() == queued_jobs; }); + // ------------------------------------------------------------------------- + // 3. try_submit() — non-blocking rejection when queue is full + // ------------------------------------------------------------------------- + logger.info("--- Test: try_submit() with bounded queue ---"); + { + // 1 slow worker, queue capacity 2 → easy to fill + espp::ThreadPool pool({ + .worker_count = 1, + .max_queue_size = 2, + .auto_start = true, + .block_on_submit_when_full = false, + .worker_task_config = { + .name = "tp_worker", + .stack_size_bytes = 4096, + .priority = 5, + .core_id = -1, + }, + .log_level = espp::Logger::Verbosity::WARN, + }); + + // Fill the worker + queue + for (int i = 0; i < 3; ++i) { + bool accepted = pool.try_submit([&]() { std::this_thread::sleep_for(200ms); }); + if (i == 0) + { + logger.info("First job accepted: {}", accepted); + } + } + + // These should be rejected (queue full) + for (int i = 0; i < 3; ++i) { + bool accepted = pool.try_submit([&]() { std::this_thread::sleep_for(50ms); }); + logger.info("try_submit when full: accepted={}", accepted); // false + } + + logger.info("Stats after try_submit test: {}", pool.stats()); + pool.stop(); } - auto stats = pool.stats(); - logger.info("ThreadPool stats: {}", stats); + // ------------------------------------------------------------------------- + // 4. submit() blocking when full (block_on_submit_when_full = true) + // ------------------------------------------------------------------------- + logger.info("--- Test: submit() blocking when queue is full ---"); + { + std::mutex done_mutex; + std::condition_variable done_cv; + std::atomic completed_jobs = 0; + constexpr int total_jobs = 6; + + espp::ThreadPool pool({ + .worker_count = 1, + .max_queue_size = 2, + .auto_start = true, + .block_on_submit_when_full = true, + .worker_task_config = { + .name = "tp_worker", + .stack_size_bytes = 4096, + .priority = 5, + .core_id = -1, + }, + .log_level = espp::Logger::Verbosity::WARN, + }); + + // Submit more jobs than capacity — submit() will block until space is free + for (int i = 0; i < total_jobs; ++i) { + bool accepted = pool.submit([&, i]() { + std::this_thread::sleep_for(50ms); + int done = ++completed_jobs; + logger.info("Blocking-submit job {} done ({}/{})", i, done, total_jobs); + done_cv.notify_one(); + }); + logger.info("Blocking-submit job {}: accepted={}", i, accepted); + } + + wait_for_jobs(done_cv, done_mutex, completed_jobs, total_jobs); + logger.info("Stats after blocking-submit test: {}", pool.stats()); + pool.stop(); + } + + // ------------------------------------------------------------------------- + // 5. submit() after stop() — rejections via is_running() guard + // ------------------------------------------------------------------------- + logger.info("--- Test: submit() on a stopped pool ---"); + { + espp::ThreadPool pool({ + .worker_count = 1, + .max_queue_size = 0, + .auto_start = true, + .worker_task_config = { + .name = "tp_worker", + .stack_size_bytes = 4096, + .priority = 5, + .core_id = -1, + }, + .log_level = espp::Logger::Verbosity::WARN, + }); + + pool.stop(); + bool accepted = pool.submit([]() {}); + logger.info("submit after stop: accepted={} (expected false)", accepted); + logger.info("Stats after stopped-pool test: {}", pool.stats()); + } - pool.stop(); logger.info("ThreadPool example complete"); while (true) { From e3ac58f4efabffd494f1ca432c3994e59105f14e Mon Sep 17 00:00:00 2001 From: Guo Date: Thu, 23 Jul 2026 10:35:56 -0500 Subject: [PATCH 17/24] fix AI review; move format helper to a header file --- components/thread_pool/include/thread_pool.hpp | 14 +------------- .../include/thread_pool_format_helpers.hpp | 17 +++++++++++++++++ components/thread_pool/src/thread_pool.cpp | 6 +++--- 3 files changed, 21 insertions(+), 16 deletions(-) create mode 100644 components/thread_pool/include/thread_pool_format_helpers.hpp diff --git a/components/thread_pool/include/thread_pool.hpp b/components/thread_pool/include/thread_pool.hpp index f5f796ba7..26e41e1f0 100644 --- a/components/thread_pool/include/thread_pool.hpp +++ b/components/thread_pool/include/thread_pool.hpp @@ -119,16 +119,4 @@ class ThreadPool : public espp::BaseComponent { } // namespace espp -// fmt formatter for espp::ThreadPool::Stats -template <> struct fmt::formatter { - template constexpr auto parse(ParseContext &ctx) const { - return ctx.begin(); - } - - template - auto format(espp::ThreadPool::Stats const &s, FormatContext &ctx) const { - return fmt::format_to(ctx.out(), - "ThreadPool::Stats{{submitted: {}, executed: {}, rejected: {}}}", - s.submitted, s.executed, s.rejected); - } -}; +#include "thread_pool_format_helpers.hpp" diff --git a/components/thread_pool/include/thread_pool_format_helpers.hpp b/components/thread_pool/include/thread_pool_format_helpers.hpp new file mode 100644 index 000000000..bb7fe069b --- /dev/null +++ b/components/thread_pool/include/thread_pool_format_helpers.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "format.hpp" + +// fmt formatter for espp::ThreadPool::Stats +template <> struct fmt::formatter { + template constexpr auto parse(ParseContext &ctx) const { + return ctx.begin(); + } + + template + auto format(espp::ThreadPool::Stats const &s, FormatContext &ctx) const { + return fmt::format_to(ctx.out(), + "ThreadPool::Stats{{submitted: {}, executed: {}, rejected: {}}}", + s.submitted, s.executed, s.rejected); + } +}; \ No newline at end of file diff --git a/components/thread_pool/src/thread_pool.cpp b/components/thread_pool/src/thread_pool.cpp index 10279e02e..ea8cfdd41 100644 --- a/components/thread_pool/src/thread_pool.cpp +++ b/components/thread_pool/src/thread_pool.cpp @@ -66,12 +66,12 @@ void ThreadPool::start() { void ThreadPool::stop() { std::lock_guard lifecycle_lock(lifecycle_mutex_); - if (!running_.exchange(false)) { - return; - } { std::lock_guard lock(queue_mutex_); + if (!running_.exchange(false)) { + return; + } stopping_ = true; rejected_ += static_cast(queue_.size()); queue_.clear(); From 68787748cd555bf3448b73ed21e4574178c9aac9 Mon Sep 17 00:00:00 2001 From: Guo Date: Thu, 23 Jul 2026 11:04:14 -0500 Subject: [PATCH 18/24] update doc and fix format --- components/thread_pool/example/main/thread_pool_example.cpp | 3 +-- doc/Doxyfile | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/components/thread_pool/example/main/thread_pool_example.cpp b/components/thread_pool/example/main/thread_pool_example.cpp index 5becc8938..0276152d6 100644 --- a/components/thread_pool/example/main/thread_pool_example.cpp +++ b/components/thread_pool/example/main/thread_pool_example.cpp @@ -111,8 +111,7 @@ extern "C" void app_main(void) { // Fill the worker + queue for (int i = 0; i < 3; ++i) { bool accepted = pool.try_submit([&]() { std::this_thread::sleep_for(200ms); }); - if (i == 0) - { + if (i == 0) { logger.info("First job accepted: {}", accepted); } } diff --git a/doc/Doxyfile b/doc/Doxyfile index 63fd3069a..2dc61bd54 100755 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -169,6 +169,7 @@ EXAMPLE_PATH = \ $(PROJECT_PATH)/components/t-dongle-s3/example/main/t_dongle_s3_example.cpp \ $(PROJECT_PATH)/components/t_keyboard/example/main/t_keyboard_example.cpp \ $(PROJECT_PATH)/components/task/example/main/task_example.cpp \ + $(PROJECT_PATH)/components/thread_pool/example/main/thread_pool_example.cpp \ $(PROJECT_PATH)/components/thermistor/example/main/thermistor_example.cpp \ $(PROJECT_PATH)/components/timer/example/main/timer_example.cpp \ $(PROJECT_PATH)/components/touch/example/main/touch_example.cpp \ @@ -382,6 +383,7 @@ INPUT = \ $(PROJECT_PATH)/components/tabulate/include/tabulate.hpp \ $(PROJECT_PATH)/components/task/include/task.hpp \ $(PROJECT_PATH)/components/task/include/run_on_core.hpp \ + $(PROJECT_PATH)/components/thread_pool/include/thread_pool.hpp \ $(PROJECT_PATH)/components/thermistor/include/thermistor.hpp \ $(PROJECT_PATH)/components/timer/include/high_resolution_timer.hpp \ $(PROJECT_PATH)/components/timer/include/timer.hpp \ From 165ab76f98aaad024cd9e6a1a3e18e6849850f5f Mon Sep 17 00:00:00 2001 From: Guo Date: Thu, 23 Jul 2026 11:48:04 -0500 Subject: [PATCH 19/24] update example --- .../example/main/thread_pool_example.cpp | 174 +++++++++++++----- .../thread_pool/example/sdkconfig.defaults | 1 - 2 files changed, 129 insertions(+), 46 deletions(-) delete mode 100644 components/thread_pool/example/sdkconfig.defaults diff --git a/components/thread_pool/example/main/thread_pool_example.cpp b/components/thread_pool/example/main/thread_pool_example.cpp index 0276152d6..1f64ce4c6 100644 --- a/components/thread_pool/example/main/thread_pool_example.cpp +++ b/components/thread_pool/example/main/thread_pool_example.cpp @@ -2,7 +2,9 @@ #include #include #include +#include #include +#include #include "logger.hpp" #include "thread_pool.hpp" @@ -18,11 +20,30 @@ static void wait_for_jobs(std::condition_variable &cv, std::mutex &mtx, std::ato extern "C" void app_main(void) { espp::Logger logger({.tag = "ThreadPool Example", .level = espp::Logger::Verbosity::INFO}); + struct TestResult { + std::string name; + bool passed; + }; + std::vector results; + + // Returns false and logs on failure; used to accumulate per-test pass/fail. + auto check = [&](const std::string &test, bool condition, const std::string &desc) -> bool { + if (condition) { + logger.info(" PASS [{}]: {}", test, desc); + } else { + logger.error(" FAIL [{}]: {}", test, desc); + } + return condition; + }; + // ------------------------------------------------------------------------- // 1. Manual start/stop + is_running() + worker_count() // ------------------------------------------------------------------------- - logger.info("--- Test: auto_start=false, manual start() / stop() ---"); { + const std::string name = "lifecycle: start/stop/is_running/worker_count"; + logger.info("--- {} ---", name); + bool passed = true; + espp::ThreadPool pool({ .worker_count = 3, .max_queue_size = 0, @@ -36,29 +57,33 @@ extern "C" void app_main(void) { .log_level = espp::Logger::Verbosity::WARN, }); - logger.info("is_running before start: {}", pool.is_running()); // false - logger.info("worker_count: {}", pool.worker_count()); // 3 + passed &= check(name, !pool.is_running(), "pool should not be running before start()"); + passed &= check(name, pool.worker_count() == 3, "worker_count() should be 3"); pool.start(); - logger.info("is_running after start: {}", pool.is_running()); // true + passed &= check(name, pool.is_running(), "pool should be running after start()"); - // start() is a no-op when already running - pool.start(); - logger.info("is_running after second start: {}", pool.is_running()); // true + pool.start(); // no-op + passed &= check(name, pool.is_running(), "pool should still be running after duplicate start()"); pool.stop(); - logger.info("is_running after stop: {}", pool.is_running()); // false + passed &= check(name, !pool.is_running(), "pool should not be running after stop()"); + + results.push_back({name, passed}); } // ------------------------------------------------------------------------- - // 2. submit() — normal job dispatch, queue_size() + // 2. submit() + queue_size() + stats() // ------------------------------------------------------------------------- - logger.info("--- Test: submit() and queue_size() ---"); { + const std::string name = "submit: normal dispatch + queue_size + stats"; + logger.info("--- {} ---", name); + bool passed = true; + std::mutex done_mutex; std::condition_variable done_cv; constexpr int total_jobs = 8; - std::atomic completed_jobs = 0; + std::atomic completed_jobs{0}; espp::ThreadPool pool({ .worker_count = 2, @@ -73,27 +98,40 @@ extern "C" void app_main(void) { .log_level = espp::Logger::Verbosity::WARN, }); + int accepted_count = 0; for (int i = 0; i < total_jobs; ++i) { - bool accepted = pool.submit([&, i]() { - std::this_thread::sleep_for(50ms); - int done = ++completed_jobs; - logger.info("Job {} done ({}/{})", i, done, total_jobs); - done_cv.notify_one(); - }); - logger.info("submit job {}: accepted={}, queue_size={}", i, accepted, pool.queue_size()); + if (pool.submit([&, i]() { + std::this_thread::sleep_for(50ms); + ++completed_jobs; + done_cv.notify_one(); + })) { + ++accepted_count; + } } + passed &= check(name, accepted_count == total_jobs, "all jobs should be accepted (unbounded queue)"); wait_for_jobs(done_cv, done_mutex, completed_jobs, total_jobs); - logger.info("Stats after submit test: {}", pool.stats()); + + auto s = pool.stats(); + logger.info(" stats: {}", s); + passed &= check(name, s.submitted == total_jobs, "submitted count should equal total_jobs"); + passed &= check(name, s.executed == total_jobs, "executed count should equal total_jobs"); + passed &= check(name, s.rejected == 0, "rejected count should be 0"); + passed &= check(name, pool.queue_size() == 0, "queue should be empty after all jobs finish"); + pool.stop(); + results.push_back({name, passed}); } // ------------------------------------------------------------------------- // 3. try_submit() — non-blocking rejection when queue is full // ------------------------------------------------------------------------- - logger.info("--- Test: try_submit() with bounded queue ---"); { - // 1 slow worker, queue capacity 2 → easy to fill + const std::string name = "try_submit: rejection when queue full"; + logger.info("--- {} ---", name); + bool passed = true; + + // 1 slow worker, capacity 2: 1 executing + 2 queued = 3 slots before rejection espp::ThreadPool pool({ .worker_count = 1, .max_queue_size = 2, @@ -108,32 +146,43 @@ extern "C" void app_main(void) { .log_level = espp::Logger::Verbosity::WARN, }); - // Fill the worker + queue + // Fill worker + queue + int fill_accepted = 0; for (int i = 0; i < 3; ++i) { - bool accepted = pool.try_submit([&]() { std::this_thread::sleep_for(200ms); }); - if (i == 0) { - logger.info("First job accepted: {}", accepted); + if (pool.try_submit([&]() { std::this_thread::sleep_for(300ms); })) { + ++fill_accepted; } } + passed &= check(name, fill_accepted == 3, "first 3 try_submit calls should be accepted"); - // These should be rejected (queue full) + // These should all be rejected immediately + int rejected_count = 0; for (int i = 0; i < 3; ++i) { - bool accepted = pool.try_submit([&]() { std::this_thread::sleep_for(50ms); }); - logger.info("try_submit when full: accepted={}", accepted); // false + if (!pool.try_submit([&]() {})) { + ++rejected_count; + } } + passed &= check(name, rejected_count == 3, "try_submit when full should return false"); + + auto s = pool.stats(); + logger.info(" stats: {}", s); + passed &= check(name, s.rejected == 3, "stats.rejected should be 3"); - logger.info("Stats after try_submit test: {}", pool.stats()); pool.stop(); + results.push_back({name, passed}); } // ------------------------------------------------------------------------- // 4. submit() blocking when full (block_on_submit_when_full = true) // ------------------------------------------------------------------------- - logger.info("--- Test: submit() blocking when queue is full ---"); { + const std::string name = "submit: blocking when queue full"; + logger.info("--- {} ---", name); + bool passed = true; + std::mutex done_mutex; std::condition_variable done_cv; - std::atomic completed_jobs = 0; + std::atomic completed_jobs{0}; constexpr int total_jobs = 6; espp::ThreadPool pool({ @@ -150,27 +199,38 @@ extern "C" void app_main(void) { .log_level = espp::Logger::Verbosity::WARN, }); - // Submit more jobs than capacity — submit() will block until space is free + int accepted_count = 0; for (int i = 0; i < total_jobs; ++i) { - bool accepted = pool.submit([&, i]() { - std::this_thread::sleep_for(50ms); - int done = ++completed_jobs; - logger.info("Blocking-submit job {} done ({}/{})", i, done, total_jobs); - done_cv.notify_one(); - }); - logger.info("Blocking-submit job {}: accepted={}", i, accepted); + if (pool.submit([&, i]() { + std::this_thread::sleep_for(30ms); + ++completed_jobs; + done_cv.notify_one(); + })) { + ++accepted_count; + } } + passed &= check(name, accepted_count == total_jobs, "all jobs should be accepted (blocking submit)"); wait_for_jobs(done_cv, done_mutex, completed_jobs, total_jobs); - logger.info("Stats after blocking-submit test: {}", pool.stats()); + + auto s = pool.stats(); + logger.info(" stats: {}", s); + passed &= check(name, s.submitted == total_jobs, "submitted count should equal total_jobs"); + passed &= check(name, s.executed == total_jobs, "executed count should equal total_jobs"); + passed &= check(name, s.rejected == 0, "rejected count should be 0"); + pool.stop(); + results.push_back({name, passed}); } // ------------------------------------------------------------------------- - // 5. submit() after stop() — rejections via is_running() guard + // 5. submit() after stop() — rejected via is_running() guard // ------------------------------------------------------------------------- - logger.info("--- Test: submit() on a stopped pool ---"); { + const std::string name = "submit: rejected after stop()"; + logger.info("--- {} ---", name); + bool passed = true; + espp::ThreadPool pool({ .worker_count = 1, .max_queue_size = 0, @@ -186,11 +246,35 @@ extern "C" void app_main(void) { pool.stop(); bool accepted = pool.submit([]() {}); - logger.info("submit after stop: accepted={} (expected false)", accepted); - logger.info("Stats after stopped-pool test: {}", pool.stats()); + passed &= check(name, !accepted, "submit() after stop() should return false"); + passed &= check(name, pool.stats().submitted == 0, "submitted count should be 0"); + passed &= check(name, pool.stats().rejected == 1, "rejected count should be 1"); + + auto s = pool.stats(); + logger.info(" stats: {}", s); + results.push_back({name, passed}); } - logger.info("ThreadPool example complete"); + // ------------------------------------------------------------------------- + // Summary + // ------------------------------------------------------------------------- + logger.info("==================== Results ===================="); + int total_passed = 0; + for (const auto &r : results) { + if (r.passed) { + logger.info(" PASS {}", r.name); + ++total_passed; + } else { + logger.error(" FAIL {}", r.name); + } + } + logger.info("================================================="); + logger.info("{}/{} tests passed", total_passed, results.size()); + if (total_passed == static_cast(results.size())) { + logger.info("All tests passed!"); + } else { + logger.error("{} test(s) FAILED", static_cast(results.size()) - total_passed); + } while (true) { std::this_thread::sleep_for(1s); diff --git a/components/thread_pool/example/sdkconfig.defaults b/components/thread_pool/example/sdkconfig.defaults deleted file mode 100644 index 75ebeae1c..000000000 --- a/components/thread_pool/example/sdkconfig.defaults +++ /dev/null @@ -1 +0,0 @@ -CONFIG_COMPILER_CXX_EXCEPTIONS=y From 7959f2fe12d6c5774ecafac12fddad904186bf9b Mon Sep 17 00:00:00 2001 From: Guo Date: Thu, 23 Jul 2026 14:58:12 -0500 Subject: [PATCH 20/24] udpate test; update start function; update doc --- components/thread_pool/README.md | 3 + .../example/main/thread_pool_example.cpp | 263 +++++++++++++++++- .../thread_pool/example/sdkconfig.defaults | 4 + .../thread_pool/include/thread_pool.hpp | 6 +- .../include/thread_pool_format_helpers.hpp | 2 +- components/thread_pool/src/thread_pool.cpp | 39 +-- doc/Doxyfile | 2 +- 7 files changed, 297 insertions(+), 22 deletions(-) create mode 100644 components/thread_pool/example/sdkconfig.defaults diff --git a/components/thread_pool/README.md b/components/thread_pool/README.md index 1ffe3cbc9..95d45ce11 100644 --- a/components/thread_pool/README.md +++ b/components/thread_pool/README.md @@ -1,5 +1,7 @@ # Thread Pool Component +[![Badge](https://components.espressif.com/components/espp/thread_pool/badge.svg)](https://components.espressif.com/components/espp/thread_pool) + The `ThreadPool` component provides a reusable pool of worker tasks for executing queued jobs asynchronously. It is implemented with `espp::Task` workers and `std::condition_variable` synchronization. @@ -9,5 +11,6 @@ It is implemented with `espp::Task` workers and `std::condition_variable` synchr - Configurable worker count - Bounded or unbounded queue - Optional blocking submit mode for backpressure +- Manual `start()` / `stop()` control; `start()` returns `true` if all workers launched successfully (or the pool was already running), `false` if any worker failed to start and the pool was rolled back to stopped state - Graceful stop (stops workers; queued jobs may not be executed) - Thread-safe stats for submitted / executed / rejected jobs diff --git a/components/thread_pool/example/main/thread_pool_example.cpp b/components/thread_pool/example/main/thread_pool_example.cpp index 1f64ce4c6..7227522f6 100644 --- a/components/thread_pool/example/main/thread_pool_example.cpp +++ b/components/thread_pool/example/main/thread_pool_example.cpp @@ -60,10 +60,10 @@ extern "C" void app_main(void) { passed &= check(name, !pool.is_running(), "pool should not be running before start()"); passed &= check(name, pool.worker_count() == 3, "worker_count() should be 3"); - pool.start(); + passed &= check(name, pool.start(), "start() should return true on first call"); passed &= check(name, pool.is_running(), "pool should be running after start()"); - pool.start(); // no-op + passed &= check(name, pool.start(), "start() should return true when already running (no-op)"); passed &= check(name, pool.is_running(), "pool should still be running after duplicate start()"); pool.stop(); @@ -255,6 +255,265 @@ extern "C" void app_main(void) { results.push_back({name, passed}); } + // ------------------------------------------------------------------------- + // 6. Concurrent start/stop from multiple threads + // ------------------------------------------------------------------------- + { + const std::string name = "concurrent: start/stop from multiple threads"; + logger.info("--- {} ---", name); + bool passed = true; + + espp::ThreadPool pool({ + .worker_count = 2, + .max_queue_size = 0, + .auto_start = false, + .worker_task_config = { + .name = "tp_worker", + .stack_size_bytes = 4096, + .priority = 5, + .core_id = -1, + }, + .log_level = espp::Logger::Verbosity::WARN, + }); + + constexpr int num_threads = 4; + constexpr int iterations = 10; + std::vector threads; + threads.reserve(num_threads); + + for (int t = 0; t < num_threads; ++t) { + threads.emplace_back([&pool, t]() { + for (int i = 0; i < iterations; ++i) { + if ((t + i) % 2 == 0) { + pool.start(); + } else { + pool.stop(); + } + } + }); + } + for (auto &t : threads) { + t.join(); + } + + // Bring pool to a known stopped state and verify consistency + pool.stop(); + passed &= check(name, !pool.is_running(), "pool should reach a clean stopped state"); + + auto s = pool.stats(); + logger.info(" stats: {}", s); + // No jobs were submitted — all counters must be zero + passed &= check(name, s.submitted == 0 && s.executed == 0 && s.rejected == 0, + "stats should all be zero (no jobs submitted)"); + + results.push_back({name, passed}); + } + + // ------------------------------------------------------------------------- + // 7. Concurrent submit/try_submit from multiple producer threads + // ------------------------------------------------------------------------- + { + const std::string name = "concurrent: multi-thread submit and try_submit"; + logger.info("--- {} ---", name); + bool passed = true; + + std::mutex done_mutex; + std::condition_variable done_cv; + constexpr int num_submit_threads = 3; + constexpr int num_try_submit_threads = 2; + constexpr int jobs_per_thread = 10; + constexpr int total_jobs = + (num_submit_threads + num_try_submit_threads) * jobs_per_thread; + std::atomic completed_jobs{0}; + std::atomic total_accepted{0}; + + espp::ThreadPool pool({ + .worker_count = 4, + .max_queue_size = 0, + .auto_start = true, + .worker_task_config = { + .name = "tp_worker", + .stack_size_bytes = 4096, + .priority = 5, + .core_id = -1, + }, + .log_level = espp::Logger::Verbosity::WARN, + }); + + std::vector producers; + producers.reserve(num_submit_threads + num_try_submit_threads); + + // submit() producers + for (int p = 0; p < num_submit_threads; ++p) { + producers.emplace_back([&]() { + for (int i = 0; i < jobs_per_thread; ++i) { + if (pool.submit([&]() { + std::this_thread::sleep_for(10ms); + ++completed_jobs; + done_cv.notify_one(); + })) { + ++total_accepted; + } + } + }); + } + + // try_submit() producers + for (int p = 0; p < num_try_submit_threads; ++p) { + producers.emplace_back([&]() { + for (int i = 0; i < jobs_per_thread; ++i) { + if (pool.try_submit([&]() { + std::this_thread::sleep_for(10ms); + ++completed_jobs; + done_cv.notify_one(); + })) { + ++total_accepted; + } + } + }); + } + + for (auto &p : producers) { + p.join(); + } + + wait_for_jobs(done_cv, done_mutex, completed_jobs, total_accepted.load()); + + auto s = pool.stats(); + logger.info(" stats: {}", s); + passed &= check(name, s.submitted + s.rejected == total_jobs, + "submitted + rejected should equal total attempted"); + passed &= check(name, s.executed == s.submitted, + "all accepted jobs should be executed (unbounded queue)"); + passed &= check(name, s.rejected == 0, "unbounded queue should not reject any jobs"); + + pool.stop(); + results.push_back({name, passed}); + } + + // ------------------------------------------------------------------------- + // 8. Chained pools: a job in pool A submits work to pool B + // ------------------------------------------------------------------------- + { + const std::string name = "chained: job in pool_a submits to pool_b"; + logger.info("--- {} ---", name); + bool passed = true; + + std::mutex done_mutex; + std::condition_variable done_cv; + constexpr int num_a_jobs = 5; + constexpr int b_jobs_per_a = 2; + constexpr int total_b_jobs = num_a_jobs * b_jobs_per_a; + std::atomic completed_b{0}; + + espp::ThreadPool pool_b({ + .worker_count = 2, + .max_queue_size = 0, + .auto_start = true, + .worker_task_config = { + .name = "pool_b_worker", + .stack_size_bytes = 4096, + .priority = 5, + .core_id = -1, + }, + .log_level = espp::Logger::Verbosity::WARN, + }); + + espp::ThreadPool pool_a({ + .worker_count = 2, + .max_queue_size = 0, + .auto_start = true, + .worker_task_config = { + .name = "pool_a_worker", + .stack_size_bytes = 4096, + .priority = 5, + .core_id = -1, + }, + .log_level = espp::Logger::Verbosity::WARN, + }); + + for (int i = 0; i < num_a_jobs; ++i) { + pool_a.submit([&pool_b, &completed_b, &done_cv]() { + for (int j = 0; j < b_jobs_per_a; ++j) { + pool_b.submit([&completed_b, &done_cv]() { + std::this_thread::sleep_for(20ms); + ++completed_b; + done_cv.notify_one(); + }); + } + }); + } + + wait_for_jobs(done_cv, done_mutex, completed_b, total_b_jobs); + + auto sa = pool_a.stats(); + auto sb = pool_b.stats(); + logger.info(" pool_a stats: {}", sa); + logger.info(" pool_b stats: {}", sb); + passed &= check(name, sa.executed == num_a_jobs, "pool_a should execute all A jobs"); + passed &= check(name, sb.executed == total_b_jobs, "pool_b should execute all chained B jobs"); + passed &= check(name, sb.rejected == 0, "pool_b should not reject any jobs"); + + pool_a.stop(); + pool_b.stop(); + results.push_back({name, passed}); + } + + // ------------------------------------------------------------------------- + // 9. Self-submit: a job submits another job back to the same pool + // ------------------------------------------------------------------------- + { + const std::string name = "self-submit: job submits to its own pool"; + logger.info("--- {} ---", name); + bool passed = true; + + std::mutex done_mutex; + std::condition_variable done_cv; + constexpr int num_initial_jobs = 4; + // Each initial job submits one follow-up job → 4 initial + 4 follow-up = 8 total + constexpr int total_executions = num_initial_jobs * 2; + std::atomic completed{0}; + + espp::ThreadPool pool({ + .worker_count = 2, + .max_queue_size = 0, + .auto_start = true, + .worker_task_config = { + .name = "tp_worker", + .stack_size_bytes = 4096, + .priority = 5, + .core_id = -1, + }, + .log_level = espp::Logger::Verbosity::WARN, + }); + + for (int i = 0; i < num_initial_jobs; ++i) { + pool.submit([&pool, &completed, &done_cv]() { + ++completed; + done_cv.notify_one(); + // Submit a follow-up job back to the same pool without deadlock + pool.submit([&completed, &done_cv]() { + std::this_thread::sleep_for(10ms); + ++completed; + done_cv.notify_one(); + }); + }); + } + + wait_for_jobs(done_cv, done_mutex, completed, total_executions); + + auto s = pool.stats(); + logger.info(" stats: {}", s); + passed &= check(name, s.submitted == total_executions, + "all initial + follow-up jobs should be submitted"); + passed &= check(name, s.executed == total_executions, + "all initial + follow-up jobs should be executed"); + passed &= check(name, s.rejected == 0, "no jobs should be rejected"); + + pool.stop(); + results.push_back({name, passed}); + } + // ------------------------------------------------------------------------- // Summary // ------------------------------------------------------------------------- diff --git a/components/thread_pool/example/sdkconfig.defaults b/components/thread_pool/example/sdkconfig.defaults new file mode 100644 index 000000000..c3667f3e3 --- /dev/null +++ b/components/thread_pool/example/sdkconfig.defaults @@ -0,0 +1,4 @@ +# Common ESP-related +# +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 +CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 diff --git a/components/thread_pool/include/thread_pool.hpp b/components/thread_pool/include/thread_pool.hpp index 26e41e1f0..201fd4f7c 100644 --- a/components/thread_pool/include/thread_pool.hpp +++ b/components/thread_pool/include/thread_pool.hpp @@ -57,8 +57,10 @@ class ThreadPool : public espp::BaseComponent { ~ThreadPool(); /// @brief Start all worker threads. - /// @note No-op if the pool is already running. - void start(); + /// @return true if at all workers were successfully started, false otherwise. + /// @note No-op if the pool is already running and return true immediately. + /// @note If any workers could not be started, the pool will roll back to the stopped state. + bool start(); /// @brief Stop all worker threads and reject further submissions. /// @note Blocks until every worker has exited; queued jobs may not be executed. diff --git a/components/thread_pool/include/thread_pool_format_helpers.hpp b/components/thread_pool/include/thread_pool_format_helpers.hpp index bb7fe069b..f301b10fb 100644 --- a/components/thread_pool/include/thread_pool_format_helpers.hpp +++ b/components/thread_pool/include/thread_pool_format_helpers.hpp @@ -14,4 +14,4 @@ template <> struct fmt::formatter { "ThreadPool::Stats{{submitted: {}, executed: {}, rejected: {}}}", s.submitted, s.executed, s.rejected); } -}; \ No newline at end of file +}; diff --git a/components/thread_pool/src/thread_pool.cpp b/components/thread_pool/src/thread_pool.cpp index ea8cfdd41..34ff2cdaf 100644 --- a/components/thread_pool/src/thread_pool.cpp +++ b/components/thread_pool/src/thread_pool.cpp @@ -28,40 +28,44 @@ ThreadPool::ThreadPool(const Config &config) ThreadPool::~ThreadPool() { stop(); } -void ThreadPool::start() { +bool ThreadPool::start() { std::lock_guard lifecycle_lock(lifecycle_mutex_); { std::lock_guard lock(queue_mutex_); if (running_.load()) { - return; + return true; } stopping_ = false; - running_.store(true); } - std::size_t started_count = 0; + bool all_workers_started = true; for (std::size_t i = 0; i < workers_.size(); ++i) { - if (workers_[i]->start()) { - ++started_count; - } else { + if ((!workers_[i]->start()) && (!workers_[i]->is_running())) { logger_.warn("Failed to start worker {} (already started or insufficient memory)", i); + all_workers_started = false; + break; } } - if (started_count == 0) { - logger_.error("No workers started; rolling back pool start"); - for (auto &worker : workers_) { - worker->stop(); - } + if (!all_workers_started) { + logger_.error("Not all workers started; rolling back pool start"); { std::lock_guard lock(queue_mutex_); - running_.store(false); stopping_ = true; - rejected_ += static_cast(queue_.size()); - queue_.clear(); } - queue_has_space_cv_.notify_all(); + queue_has_work_cv_.notify_all(); + queue_has_space_cv_.notify_all(); // thread should stopped after notify all. + for (auto &worker : workers_) { + worker->stop(); // stop and join heres + } + return false; } + + { + std::lock_guard lock(queue_mutex_); + running_.store(true); + } + return true; } void ThreadPool::stop() { @@ -69,6 +73,9 @@ void ThreadPool::stop() { { std::lock_guard lock(queue_mutex_); + if (!running_.load()) { + return; + } if (!running_.exchange(false)) { return; } diff --git a/doc/Doxyfile b/doc/Doxyfile index 2dc61bd54..977cc2f76 100755 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -383,8 +383,8 @@ INPUT = \ $(PROJECT_PATH)/components/tabulate/include/tabulate.hpp \ $(PROJECT_PATH)/components/task/include/task.hpp \ $(PROJECT_PATH)/components/task/include/run_on_core.hpp \ - $(PROJECT_PATH)/components/thread_pool/include/thread_pool.hpp \ $(PROJECT_PATH)/components/thermistor/include/thermistor.hpp \ + $(PROJECT_PATH)/components/thread_pool/include/thread_pool.hpp \ $(PROJECT_PATH)/components/timer/include/high_resolution_timer.hpp \ $(PROJECT_PATH)/components/timer/include/timer.hpp \ $(PROJECT_PATH)/components/touch/include/touch.hpp \ From acd6f8ec5931f9a09997c6b39e3d34cfab46bdfe Mon Sep 17 00:00:00 2001 From: Guo Date: Thu, 23 Jul 2026 15:53:18 -0500 Subject: [PATCH 21/24] fix component order; fix example; update comments --- .github/workflows/build.yml | 4 +- .github/workflows/upload_components.yml | 2 +- .../example/main/thread_pool_example.cpp | 53 +++++++++++++++++-- .../thread_pool/include/thread_pool.hpp | 4 +- doc/Doxyfile | 2 +- 5 files changed, 54 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8756e7869..62f751f92 100755 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -229,10 +229,10 @@ jobs: target: esp32 - path: 'components/task/example' target: esp32 - - path: 'components/thread_pool/example' - target: esp32 - path: 'components/thermistor/example' target: esp32 + - path: 'components/thread_pool/example' + target: esp32 - path: 'components/timer/example' target: esp32 - path: 'components/touch/example' diff --git a/.github/workflows/upload_components.yml b/.github/workflows/upload_components.yml index f4c072905..c6f873681 100755 --- a/.github/workflows/upload_components.yml +++ b/.github/workflows/upload_components.yml @@ -133,8 +133,8 @@ jobs: components/t-dongle-s3 components/tabulate components/task - components/thread_pool components/thermistor + components/thread_pool components/timer components/touch components/tla2528 diff --git a/components/thread_pool/example/main/thread_pool_example.cpp b/components/thread_pool/example/main/thread_pool_example.cpp index 7227522f6..ab2edbcab 100644 --- a/components/thread_pool/example/main/thread_pool_example.cpp +++ b/components/thread_pool/example/main/thread_pool_example.cpp @@ -131,7 +131,34 @@ extern "C" void app_main(void) { logger.info("--- {} ---", name); bool passed = true; - // 1 slow worker, capacity 2: 1 executing + 2 queued = 3 slots before rejection + // 1 worker, queue capacity 2: 1 executing + 2 queued = 3 total slots. + // + // Jobs are gated by an explicit barrier so workers cannot drain the queue + // before we assert rejection, making the test fully deterministic. + // + // To guarantee the queue is provably full we must also ensure the worker + // has dequeued (and is executing) the first job before we fill the two + // remaining queue slots. We use a separate "started" CV for this. + std::mutex barrier_mutex; + std::condition_variable barrier_cv; + bool release_workers = false; + + std::mutex started_mutex; + std::condition_variable started_cv; + std::atomic jobs_started{0}; + + auto blocking_job = [&]() { + // Signal that this job is now executing (off the queue) + { + std::lock_guard lock(started_mutex); + ++jobs_started; + } + started_cv.notify_one(); + // Block until the test releases the barrier + std::unique_lock lock(barrier_mutex); + barrier_cv.wait(lock, [&]() { return release_workers; }); + }; + espp::ThreadPool pool({ .worker_count = 1, .max_queue_size = 2, @@ -146,16 +173,26 @@ extern "C" void app_main(void) { .log_level = espp::Logger::Verbosity::WARN, }); - // Fill worker + queue + // Step 1: submit the first job and wait until it is executing + // (it has been removed from the queue by the worker). int fill_accepted = 0; - for (int i = 0; i < 3; ++i) { - if (pool.try_submit([&]() { std::this_thread::sleep_for(300ms); })) { + if (pool.try_submit(blocking_job)) { + ++fill_accepted; + } + { + std::unique_lock lock(started_mutex); + started_cv.wait(lock, [&]() { return jobs_started.load() >= 1; }); + } + + // Step 2: fill the 2 remaining queue slots. + for (int i = 0; i < 2; ++i) { + if (pool.try_submit(blocking_job)) { ++fill_accepted; } } passed &= check(name, fill_accepted == 3, "first 3 try_submit calls should be accepted"); - // These should all be rejected immediately + // Step 3: queue is now provably full — every additional try_submit must be rejected. int rejected_count = 0; for (int i = 0; i < 3; ++i) { if (!pool.try_submit([&]() {})) { @@ -168,6 +205,12 @@ extern "C" void app_main(void) { logger.info(" stats: {}", s); passed &= check(name, s.rejected == 3, "stats.rejected should be 3"); + // Release the barrier so workers can finish, then stop cleanly. + { + std::lock_guard lock(barrier_mutex); + release_workers = true; + } + barrier_cv.notify_all(); pool.stop(); results.push_back({name, passed}); } diff --git a/components/thread_pool/include/thread_pool.hpp b/components/thread_pool/include/thread_pool.hpp index 201fd4f7c..393e32fb2 100644 --- a/components/thread_pool/include/thread_pool.hpp +++ b/components/thread_pool/include/thread_pool.hpp @@ -31,7 +31,7 @@ class ThreadPool : public espp::BaseComponent { struct Stats { std::uint64_t submitted = 0; ///< Total jobs accepted into the queue. std::uint64_t executed = 0; ///< Total jobs successfully executed. - std::uint64_t rejected = 0; ///< Total jobs rejected (invalid job, stopped/stopping, or queue full). + std::uint64_t rejected = 0; ///< Total jobs rejected (invalid job, stopped/stopping, or queue full) or dropped (due to stop, the enqueued jobs were dropped). }; /// @brief Configuration parameters for constructing a ThreadPool. @@ -57,7 +57,7 @@ class ThreadPool : public espp::BaseComponent { ~ThreadPool(); /// @brief Start all worker threads. - /// @return true if at all workers were successfully started, false otherwise. + /// @return True if all workers were successfully started, false otherwise. /// @note No-op if the pool is already running and return true immediately. /// @note If any workers could not be started, the pool will roll back to the stopped state. bool start(); diff --git a/doc/Doxyfile b/doc/Doxyfile index 977cc2f76..e5b3f886e 100755 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -169,8 +169,8 @@ EXAMPLE_PATH = \ $(PROJECT_PATH)/components/t-dongle-s3/example/main/t_dongle_s3_example.cpp \ $(PROJECT_PATH)/components/t_keyboard/example/main/t_keyboard_example.cpp \ $(PROJECT_PATH)/components/task/example/main/task_example.cpp \ - $(PROJECT_PATH)/components/thread_pool/example/main/thread_pool_example.cpp \ $(PROJECT_PATH)/components/thermistor/example/main/thermistor_example.cpp \ + $(PROJECT_PATH)/components/thread_pool/example/main/thread_pool_example.cpp \ $(PROJECT_PATH)/components/timer/example/main/timer_example.cpp \ $(PROJECT_PATH)/components/touch/example/main/touch_example.cpp \ $(PROJECT_PATH)/components/tla2528/example/main/tla2528_example.cpp \ From 34c6c87a07984d066b83a7bcf15513b64daa579f Mon Sep 17 00:00:00 2001 From: Guo Date: Thu, 23 Jul 2026 22:24:46 -0500 Subject: [PATCH 22/24] fix typo --- components/thread_pool/src/thread_pool.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/thread_pool/src/thread_pool.cpp b/components/thread_pool/src/thread_pool.cpp index 34ff2cdaf..8efcccb5b 100644 --- a/components/thread_pool/src/thread_pool.cpp +++ b/components/thread_pool/src/thread_pool.cpp @@ -54,9 +54,9 @@ bool ThreadPool::start() { stopping_ = true; } queue_has_work_cv_.notify_all(); - queue_has_space_cv_.notify_all(); // thread should stopped after notify all. + queue_has_space_cv_.notify_all(); // threads should be stopped after notify all. for (auto &worker : workers_) { - worker->stop(); // stop and join heres + worker->stop(); // stop and join here } return false; } From 8c4f0f95dea0488c632335d1524ba7d5d35e6c8b Mon Sep 17 00:00:00 2001 From: Guo Date: Fri, 24 Jul 2026 09:29:36 -0500 Subject: [PATCH 23/24] update readme; add example code link --- components/thread_pool/example/README.md | 35 ++++++++++++++++--- .../example/main/thread_pool_example.cpp | 18 ++++++++++ .../thread_pool/include/thread_pool.hpp | 33 +++++++++++++---- 3 files changed, 75 insertions(+), 11 deletions(-) diff --git a/components/thread_pool/example/README.md b/components/thread_pool/example/README.md index 4c0790c06..a34dd22cb 100644 --- a/components/thread_pool/example/README.md +++ b/components/thread_pool/example/README.md @@ -1,11 +1,36 @@ # Thread Pool Example -This example shows how to: +This example is a self-contained test suite that exercises the full public API +of `espp::ThreadPool` across a range of usage patterns, from basic single-pool +operation to more advanced concurrent and multi-pool scenarios. -- Create an `espp::ThreadPool` -- Submit jobs -- Wait for all jobs to complete -- Read execution statistics +| # | Test | APIs covered | +|---|---|---| +| 1 | Lifecycle | `start()`, `stop()`, `is_running()`, `worker_count()` | +| 2 | Normal dispatch | `submit()`, `queue_size()`, `stats()` | +| 3 | Bounded-queue rejection | `try_submit()` with deterministic barrier | +| 4 | Blocking submit | `submit()` with `block_on_submit_when_full` | +| 5 | Post-stop rejection | `submit()` after `stop()` | +| 6 | Concurrent lifecycle | multi-thread `start()` / `stop()` stress | +| 7 | Concurrent submission | multi-thread `submit()` + `try_submit()` | +| 8 | Chained pools | a job in `pool_a` submitting work into `pool_b` | +| 9 | Self-submit | a running job submitting back to its own pool | + +Each test logs individual `PASS` / `FAIL` results inline. At the end of the run +a summary is printed: + +``` +==================== Results ==================== + PASS lifecycle: start/stop/is_running/worker_count + PASS submit: normal dispatch + queue_size + stats + ... +================================================= +9/9 tests passed +All tests passed! +``` + +The final summary line (`N/N tests passed` / `N test(s) FAILED`) is the key +indicator of overall correctness and is suitable for CI log parsing. ## Build diff --git a/components/thread_pool/example/main/thread_pool_example.cpp b/components/thread_pool/example/main/thread_pool_example.cpp index ab2edbcab..3d33a0dfb 100644 --- a/components/thread_pool/example/main/thread_pool_example.cpp +++ b/components/thread_pool/example/main/thread_pool_example.cpp @@ -41,6 +41,7 @@ extern "C" void app_main(void) { // ------------------------------------------------------------------------- { const std::string name = "lifecycle: start/stop/is_running/worker_count"; + //! [lifecycle example] logger.info("--- {} ---", name); bool passed = true; @@ -68,6 +69,7 @@ extern "C" void app_main(void) { pool.stop(); passed &= check(name, !pool.is_running(), "pool should not be running after stop()"); + //! [lifecycle example] results.push_back({name, passed}); } @@ -77,6 +79,7 @@ extern "C" void app_main(void) { // ------------------------------------------------------------------------- { const std::string name = "submit: normal dispatch + queue_size + stats"; + //! [submit example] logger.info("--- {} ---", name); bool passed = true; @@ -120,6 +123,7 @@ extern "C" void app_main(void) { passed &= check(name, pool.queue_size() == 0, "queue should be empty after all jobs finish"); pool.stop(); + //! [submit example] results.push_back({name, passed}); } @@ -128,6 +132,7 @@ extern "C" void app_main(void) { // ------------------------------------------------------------------------- { const std::string name = "try_submit: rejection when queue full"; + //! [try_submit example] logger.info("--- {} ---", name); bool passed = true; @@ -212,6 +217,7 @@ extern "C" void app_main(void) { } barrier_cv.notify_all(); pool.stop(); + //! [try_submit example] results.push_back({name, passed}); } @@ -220,6 +226,7 @@ extern "C" void app_main(void) { // ------------------------------------------------------------------------- { const std::string name = "submit: blocking when queue full"; + //! [blocking submit example] logger.info("--- {} ---", name); bool passed = true; @@ -263,6 +270,7 @@ extern "C" void app_main(void) { passed &= check(name, s.rejected == 0, "rejected count should be 0"); pool.stop(); + //! [blocking submit example] results.push_back({name, passed}); } @@ -271,6 +279,7 @@ extern "C" void app_main(void) { // ------------------------------------------------------------------------- { const std::string name = "submit: rejected after stop()"; + //! [submit after stop example] logger.info("--- {} ---", name); bool passed = true; @@ -295,6 +304,7 @@ extern "C" void app_main(void) { auto s = pool.stats(); logger.info(" stats: {}", s); + //! [submit after stop example] results.push_back({name, passed}); } @@ -303,6 +313,7 @@ extern "C" void app_main(void) { // ------------------------------------------------------------------------- { const std::string name = "concurrent: start/stop from multiple threads"; + //! [concurrent lifecycle example] logger.info("--- {} ---", name); bool passed = true; @@ -348,6 +359,7 @@ extern "C" void app_main(void) { // No jobs were submitted — all counters must be zero passed &= check(name, s.submitted == 0 && s.executed == 0 && s.rejected == 0, "stats should all be zero (no jobs submitted)"); + //! [concurrent lifecycle example] results.push_back({name, passed}); } @@ -357,6 +369,7 @@ extern "C" void app_main(void) { // ------------------------------------------------------------------------- { const std::string name = "concurrent: multi-thread submit and try_submit"; + //! [concurrent submit example] logger.info("--- {} ---", name); bool passed = true; @@ -431,6 +444,7 @@ extern "C" void app_main(void) { passed &= check(name, s.rejected == 0, "unbounded queue should not reject any jobs"); pool.stop(); + //! [concurrent submit example] results.push_back({name, passed}); } @@ -439,6 +453,7 @@ extern "C" void app_main(void) { // ------------------------------------------------------------------------- { const std::string name = "chained: job in pool_a submits to pool_b"; + //! [chained pools example] logger.info("--- {} ---", name); bool passed = true; @@ -499,6 +514,7 @@ extern "C" void app_main(void) { pool_a.stop(); pool_b.stop(); + //! [chained pools example] results.push_back({name, passed}); } @@ -507,6 +523,7 @@ extern "C" void app_main(void) { // ------------------------------------------------------------------------- { const std::string name = "self-submit: job submits to its own pool"; + //! [self-submit example] logger.info("--- {} ---", name); bool passed = true; @@ -554,6 +571,7 @@ extern "C" void app_main(void) { passed &= check(name, s.rejected == 0, "no jobs should be rejected"); pool.stop(); + //! [self-submit example] results.push_back({name, passed}); } diff --git a/components/thread_pool/include/thread_pool.hpp b/components/thread_pool/include/thread_pool.hpp index 393e32fb2..d67f25469 100644 --- a/components/thread_pool/include/thread_pool.hpp +++ b/components/thread_pool/include/thread_pool.hpp @@ -16,12 +16,33 @@ namespace espp { -/// @brief A thread pool that dispatches submitted jobs to a fixed set of worker threads. -/// -/// Workers are implemented as espp::Task instances. Jobs are queued and -/// consumed in FIFO order. The queue can be optionally bounded; when full, -/// new submissions are either rejected immediately or blocked until space -/// becomes available, depending on the configuration. +/** + * @brief A thread pool that dispatches submitted jobs to a fixed set of worker threads. + * + * Workers are implemented as espp::Task instances. Jobs are queued and + * consumed in FIFO order. The queue can be optionally bounded; when full, + * new submissions are either rejected immediately or blocked until space + * becomes available, depending on the configuration. + * + * \section thread_pool_ex1 Lifecycle: start / stop / is_running / worker_count + * \snippet thread_pool_example.cpp lifecycle example + * \section thread_pool_ex2 Submit Jobs + * \snippet thread_pool_example.cpp submit example + * \section thread_pool_ex3 try_submit — Non-Blocking Rejection When Full + * \snippet thread_pool_example.cpp try_submit example + * \section thread_pool_ex4 Blocking Submit When Full + * \snippet thread_pool_example.cpp blocking submit example + * \section thread_pool_ex5 Submit Rejected After stop() + * \snippet thread_pool_example.cpp submit after stop example + * \section thread_pool_ex6 Concurrent start / stop + * \snippet thread_pool_example.cpp concurrent lifecycle example + * \section thread_pool_ex7 Concurrent submit and try_submit + * \snippet thread_pool_example.cpp concurrent submit example + * \section thread_pool_ex8 Chained Pools + * \snippet thread_pool_example.cpp chained pools example + * \section thread_pool_ex9 Self-Submit + * \snippet thread_pool_example.cpp self-submit example + */ class ThreadPool : public espp::BaseComponent { public: /// @brief A callable job that can be submitted to the pool. From 6862c787b515799a87f6e223da94aae0bc309523 Mon Sep 17 00:00:00 2001 From: Guo Date: Fri, 24 Jul 2026 10:31:11 -0500 Subject: [PATCH 24/24] change signature to only accept r-value; use atomic for stopping and running and remove mutex --- .../thread_pool/include/thread_pool.hpp | 8 ++-- components/thread_pool/src/thread_pool.cpp | 42 +++++++------------ 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/components/thread_pool/include/thread_pool.hpp b/components/thread_pool/include/thread_pool.hpp index d67f25469..41fd9336d 100644 --- a/components/thread_pool/include/thread_pool.hpp +++ b/components/thread_pool/include/thread_pool.hpp @@ -97,14 +97,14 @@ class ThreadPool : public espp::BaseComponent { /// reached its capacity limit. Otherwise behaves identically to try_submit(). /// @param job Callable to enqueue; moved into the queue on acceptance. /// @return true if the job was accepted, false if it was rejected. - bool submit(Job job); + bool submit(Job &&job); /// @brief Attempt to submit a job without blocking. /// /// Returns immediately with false when the queue is full. /// @param job Callable to enqueue; moved into the queue on acceptance. /// @return true if the job was accepted, false if it was rejected. - bool try_submit(Job job); + bool try_submit(Job &&job); /// @brief Return the number of jobs currently waiting in the queue. /// @return Pending job count. @@ -121,7 +121,7 @@ class ThreadPool : public espp::BaseComponent { private: bool worker_task_fn(); - bool submit_impl(Job job, bool allow_blocking_when_full); + bool submit_impl(Job &&job, bool allow_blocking_when_full); Config config_; @@ -133,7 +133,7 @@ class ThreadPool : public espp::BaseComponent { std::vector> workers_; std::atomic running_{false}; - bool stopping_ = false; + std::atomic stopping_{false}; std::atomic submitted_{0}; std::atomic executed_{0}; diff --git a/components/thread_pool/src/thread_pool.cpp b/components/thread_pool/src/thread_pool.cpp index 8efcccb5b..d42a0ba38 100644 --- a/components/thread_pool/src/thread_pool.cpp +++ b/components/thread_pool/src/thread_pool.cpp @@ -30,13 +30,11 @@ ThreadPool::~ThreadPool() { stop(); } bool ThreadPool::start() { std::lock_guard lifecycle_lock(lifecycle_mutex_); - { - std::lock_guard lock(queue_mutex_); - if (running_.load()) { - return true; - } - stopping_ = false; + + if (running_.load()) { + return true; } + stopping_ = false; bool all_workers_started = true; for (std::size_t i = 0; i < workers_.size(); ++i) { @@ -49,37 +47,29 @@ bool ThreadPool::start() { if (!all_workers_started) { logger_.error("Not all workers started; rolling back pool start"); - { - std::lock_guard lock(queue_mutex_); - stopping_ = true; - } + stopping_ = true; queue_has_work_cv_.notify_all(); - queue_has_space_cv_.notify_all(); // threads should be stopped after notify all. + queue_has_space_cv_.notify_all(); for (auto &worker : workers_) { - worker->stop(); // stop and join here + worker->stop(); } return false; } - { - std::lock_guard lock(queue_mutex_); - running_.store(true); - } + running_.store(true); return true; } void ThreadPool::stop() { std::lock_guard lifecycle_lock(lifecycle_mutex_); + if (!running_.exchange(false)) { + return; + } + stopping_ = true; + { std::lock_guard lock(queue_mutex_); - if (!running_.load()) { - return; - } - if (!running_.exchange(false)) { - return; - } - stopping_ = true; rejected_ += static_cast(queue_.size()); queue_.clear(); } @@ -94,13 +84,13 @@ void ThreadPool::stop() { bool ThreadPool::is_running() const { return running_.load(); } -bool ThreadPool::submit(Job job) { +bool ThreadPool::submit(Job &&job) { return submit_impl(std::move(job), config_.block_on_submit_when_full); } -bool ThreadPool::try_submit(Job job) { return submit_impl(std::move(job), false); } +bool ThreadPool::try_submit(Job &&job) { return submit_impl(std::move(job), false); } -bool ThreadPool::submit_impl(Job job, bool allow_blocking_when_full) { +bool ThreadPool::submit_impl(Job &&job, bool allow_blocking_when_full) { if (!job) { rejected_++; return false;