Thread Pool APIs

ThreadPool

The espp::ThreadPool component provides a reusable pool of worker tasks for executing queued jobs asynchronously. Workers are implemented as espp::Task instances and pull work from internal job queues whose combined maximum size is optionally enforced by 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.

Priority bands

Jobs can be submitted at one of four espp::QosBand priority bands — Critical / High / Normal / Low — via submit(job, band) / try_submit(job, band). Internally the pool keeps one FIFO queue per band and workers always pop the most urgent non-empty band first; the band-less submit(job) overload uses Normal, so code that does not use bands behaves exactly as before. Per-band submitted / executed / aged / rejected counters are reported through espp::ThreadPool::Stats.

To keep a busy high band from starving lower bands, a queued job whose wait exceeds espp::ThreadPool::Config::aging_threshold (default 100 ms) is aged: promoted up one band, to the back of that band’s queue. Setting the threshold to 0 disables aging (strict band priority).

Per-band workers

By default all workers are identical and serve every band. Setting espp::ThreadPool::Config::band_worker_counts opts into dedicated per-band workers: band k gets its own workers running at espp::ThreadPool::Config::band_task_priorities [k], each servicing bands 0..k (its own band and every more urgent band), so a Critical job never waits behind more than one in-flight lower-band job. The deepest (least urgent) configured band’s workers service every band, so no band is ever unreachable — even with aging disabled.

On ESP the per-band priorities are FreeRTOS task priorities and are always applied. On host platforms (Linux / macOS) they map onto SCHED_FIFO real-time priorities, but are only applied when espp::ThreadPool::Config::band_workers_realtime is set — by default host workers run at the OS default scheduling and band ordering is enforced at the queue level only. See the espp::Task documentation for the host real-time scheduling requirements and caveats.

Code examples for the thread pool API are provided in the thread_pool example folder.

API Reference

Header File

Classes

class ThreadPool : public espp::BaseComponent

A thread pool that dispatches submitted jobs to a fixed set of worker threads, with bucketed priority bands (espp::QosBand).

Workers are implemented as espp::Task instances. Jobs are queued into one of four priority bands (QosBand::Critical/High/Normal/Low) and consumed in FIFO order within a band, with more urgent (lower-index) bands always drained first. Submitting without a band uses QosBand::Normal, which preserves the original single-FIFO-queue behavior exactly. The queue can be optionally bounded (Config::max_queue_size bounds the TOTAL across all bands); when full, new submissions are either rejected immediately or blocked until space becomes available, depending on the configuration.

**Aging (starvation guard).** With strict band priority a continuous stream of urgent jobs could starve less urgent bands forever. To prevent this, when a worker looks for work it first promotes the front entry of any band whose wait time exceeds Config::aging_threshold up one band (to the back of the next more-urgent band, with its aging clock restarted). Aging is deliberately approximate: only band fronts are examined (O(bands) per pop, no full-queue scans), and because bands are FIFO this is sufficient - the front is always the longest-waiting entry of its band. aging_threshold is an ELIGIBILITY interval, not an at-most wait bound: promotion is evaluated only when a worker next dequeues work, so a hop can happen arbitrarily later than the threshold if every worker is stuck in a long in-flight job, and after promotion the entry still waits behind the destination band’s backlog at that moment. What IS guaranteed: since promoted entries enter ahead of all later arrivals, every queued entry makes progress toward Critical under any sustained load, so nothing starves. Set aging_threshold to 0 for strict (starvation-permitting) band priority.

**Worker bands (true OS preemption, opt-in).** By default all Config::worker_count workers are identical and service every band. Setting any element of Config::band_worker_counts non-zero switches to per-band workers instead: band_worker_counts[k] workers are created for band k, each running at the espp::Task priority Config::band_task_priorities[k] (a FreeRTOS priority on ESP; mapped to a SCHED_FIFO real-time priority on Linux/macOS - see espp::Task::BaseConfig::priority). A band-k worker services bands 0..k, i.e. its own band and every MORE urgent band. This means a Critical job can be picked up by any worker, while a band-k worker never runs work less urgent than band k - with one exception: the deepest (least urgent) configured band’s workers service EVERY band, so that no band is unreachable when the configuration has no Low-band worker (see Config::band_worker_counts). The latency guarantee this buys: because every worker drains band 0 first and the band-0 workers run at the highest OS priority, a newly arrived Critical job - WHEN the Critical queue is otherwise empty - waits at most the remaining duration of one already-running job before a high-OS-priority worker picks it up (and on a preemptive OS - e.g. FreeRTOS or Linux PREEMPT_RT with granted RT scheduling - that worker preempts lower-priority ones the moment it becomes runnable). With a Critical backlog the new job additionally waits behind the earlier Critical jobs, which drain FIFO across all workers first.

Lifecycle: start / stop / is_running / worker_count

    logger.info("--- {} ---", name);
    bool passed = true;

    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,
    });

    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");

    passed &= check(name, pool.start(), "start() should return true on first call");
    passed &= check(name, pool.is_running(), "pool should be running after start()");

    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();
    passed &= check(name, !pool.is_running(), "pool should not be running after stop()");

Submit Jobs

    logger.info("--- {} ---", name);
    bool passed = true;

    std::mutex done_mutex;
    std::condition_variable done_cv;
    constexpr int total_jobs = 8;
    std::atomic<int> 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,
    });

    int accepted_count = 0;
    for (int i = 0; i < total_jobs; ++i) {
      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);

    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();

try_submit - Non-Blocking Rejection When Full

    logger.info("--- {} ---", name);
    bool passed = true;

    // 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<int> jobs_started{0};

    auto blocking_job = [&]() {
      // Signal that this job is now executing (off the queue)
      {
        std::lock_guard<std::mutex> lock(started_mutex);
        ++jobs_started;
      }
      started_cv.notify_one();
      // Block until the test releases the barrier
      std::unique_lock<std::mutex> lock(barrier_mutex);
      barrier_cv.wait(lock, [&]() { return release_workers; });
    };

    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,
    });

    // 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;
    if (pool.try_submit(blocking_job)) {
      ++fill_accepted;
    }
    {
      std::unique_lock<std::mutex> 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");

    // 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([&]() {})) {
        ++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");

    // Release the barrier so workers can finish, then stop cleanly.
    {
      std::lock_guard<std::mutex> lock(barrier_mutex);
      release_workers = true;
    }
    barrier_cv.notify_all();
    pool.stop();

Blocking Submit When Full

    logger.info("--- {} ---", name);
    bool passed = true;

    std::mutex done_mutex;
    std::condition_variable done_cv;
    std::atomic<int> 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,
    });

    int accepted_count = 0;
    for (int i = 0; i < total_jobs; ++i) {
      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);

    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();

Submit Rejected After stop()

    logger.info("--- {} ---", name);
    bool passed = true;

    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([]() {});
    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);

Concurrent start / stop

    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<std::thread> 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)");

Concurrent 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<int> completed_jobs{0};
    std::atomic<int> 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<std::thread> 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();

Chained Pools

    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<int> 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();

Self-Submit

    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<int> 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();

Priority Bands

    logger.info("--- {} ---", name);
    bool passed = true;

    // Gate the single worker on a barrier so the queue contents (and therefore
    // the dispatch order) are fully deterministic.
    std::mutex barrier_mutex;
    std::condition_variable barrier_cv;
    bool release_worker = false;
    std::mutex started_mutex;
    std::condition_variable started_cv;
    std::atomic<int> jobs_started{0};

    std::mutex done_mutex;
    std::condition_variable done_cv;
    std::atomic<int> completed_jobs{0};
    std::mutex order_mutex;
    std::vector<std::string> execution_order;

    espp::ThreadPool pool({
        .worker_count = 1,
        .auto_start = true,
        // 0 = strict band priority; the default (100ms) also promotes
        // long-waiting jobs up one band to prevent starvation ("aging").
        .aging_threshold = std::chrono::milliseconds(0),
        .worker_task_config =
            {
                .name = "tp_worker",
                .stack_size_bytes = 4096,
                .priority = 5,
                .core_id = -1,
            },
        .log_level = espp::Logger::Verbosity::WARN,
    });

    // Occupy the worker, then queue Low jobs followed by a Critical one.
    pool.submit([&]() {
      {
        std::lock_guard<std::mutex> lock(started_mutex);
        ++jobs_started;
      }
      started_cv.notify_one();
      std::unique_lock<std::mutex> lock(barrier_mutex);
      barrier_cv.wait(lock, [&]() { return release_worker; });
    });
    {
      std::unique_lock<std::mutex> lock(started_mutex);
      started_cv.wait(lock, [&]() { return jobs_started.load() >= 1; });
    }

    for (int i = 0; i < 2; ++i) {
      pool.submit(
          [&, i]() {
            {
              std::lock_guard<std::mutex> lock(order_mutex);
              execution_order.push_back("low" + std::to_string(i));
            }
            ++completed_jobs;
            done_cv.notify_one();
          },
          espp::QosBand::Low);
    }
    pool.submit(
        [&]() {
          {
            std::lock_guard<std::mutex> lock(order_mutex);
            execution_order.push_back("critical");
          }
          ++completed_jobs;
          done_cv.notify_one();
        },
        espp::QosBand::Critical);

    // Release the worker: the Critical job must run before the queued Low jobs.
    {
      std::lock_guard<std::mutex> lock(barrier_mutex);
      release_worker = true;
    }
    barrier_cv.notify_all();
    wait_for_jobs(done_cv, done_mutex, completed_jobs, 3);

    {
      std::lock_guard<std::mutex> lock(order_mutex);
      passed &= check(name, execution_order.size() == 3 && execution_order[0] == "critical",
                      "Critical job should run before the queued Low jobs");
    }
    auto s = pool.stats();
    logger.info("  stats: {}", s);
    passed &= check(name, s.band_submitted[static_cast<size_t>(espp::QosBand::Critical)] == 1,
                    "one job accounted to the Critical band");
    passed &= check(name, s.band_submitted[static_cast<size_t>(espp::QosBand::Low)] == 2,
                    "two jobs accounted to the Low band");

    pool.stop();

Public Types

using Job = std::function<void(void)>

A callable job that can be submitted to the pool.

Public Functions

explicit ThreadPool(const Config &config)

Construct the pool with the given configuration.

Parameters:

config – Pool configuration.

~ThreadPool()

Destroy the pool, stopping all workers gracefully.

bool start()

Start all worker threads.

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.

Returns:

True if all workers were successfully started, false otherwise.

void stop()

Stop all worker threads and reject further submissions.

Note

Blocks until every worker has exited; queued jobs may not be executed.

bool is_running() const

Query whether the pool is currently running.

Returns:

true if workers are active, false otherwise.

bool submit(Job &&job)

Submit a job at QosBand::Normal, 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().

Parameters:

job – Callable to enqueue; moved into the queue on acceptance.

Returns:

true if the job was accepted, false if it was rejected.

bool submit(Job &&job, QosBand band)

Submit a job at the given priority band, 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().

Parameters:
  • job – Callable to enqueue; moved into the queue on acceptance.

  • band – Priority band to enqueue the job at (see espp::QosBand).

Returns:

true if the job was accepted, false if it was rejected.

bool try_submit(Job &&job)

Attempt to submit a job at QosBand::Normal without blocking.

Returns immediately with false when the queue is full.

Parameters:

job – Callable to enqueue; moved into the queue on acceptance.

Returns:

true if the job was accepted, false if it was rejected.

bool try_submit(Job &&job, QosBand band)

Attempt to submit a job at the given priority band without blocking.

Returns immediately with false when the queue is full.

Parameters:
  • job – Callable to enqueue; moved into the queue on acceptance.

  • band – Priority band to enqueue the job at (see espp::QosBand).

Returns:

true if the job was accepted, false if it was rejected.

std::size_t queue_size() const

Return the number of jobs currently waiting in the queue (all bands).

Returns:

Pending job count.

std::size_t worker_count() const

Return the number of worker threads in the pool.

Returns:

Worker thread count.

Stats stats() const

Return a snapshot of the pool’s activity counters.

Returns:

Stats struct with total and per-band submitted / executed / rejected / aged counts.

inline const std::string &get_name() const

Get the name of the component

Note

This is the tag of the logger

Returns:

A const reference to the name of the component

inline void set_log_tag(const std::string_view &tag)

Set the tag for the logger

Parameters:

tag – The tag to use for the logger

inline espp::Logger::Verbosity get_log_level() const

Get the log level for the logger

Returns:

The verbosity level of the logger

inline void set_log_level(espp::Logger::Verbosity level)

Set the log level for the logger

Parameters:

level – The verbosity level to use for the logger

inline void set_log_verbosity(espp::Logger::Verbosity level)

Set the log verbosity for the logger

See also

set_log_level

Note

This is a convenience method that calls set_log_level

Parameters:

level – The verbosity level to use for the logger

inline espp::Logger::Verbosity get_log_verbosity() const

Get the log verbosity for the logger

See also

get_log_level

Note

This is a convenience method that calls get_log_level

Returns:

The verbosity level of the logger

inline void set_log_rate_limit(std::chrono::duration<float> rate_limit)

Set the rate limit for the logger

Note

Only calls to the logger that have _rate_limit suffix will be rate limited

Parameters:

rate_limit – The rate limit to use for the logger

Public Static Attributes

static constexpr std::size_t kNumBands = kNumQosBands

Number of priority bands (see espp::QosBand).

struct Config

Configuration parameters for constructing a ThreadPool.

Public Members

std::size_t worker_count = 1

Number of worker threads to spawn (ignored when band_worker_counts is set - see below).

std::size_t max_queue_size = 0

Maximum pending jobs TOTAL across all bands (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.

std::chrono::milliseconds aging_threshold{100}

Starvation guard: a queued job whose wait exceeds this is promoted up one band (approximate, front-of-band only - see the class docs). 0 disables aging (strict band priority).

std::array<std::size_t, kNumBands> band_worker_counts = {}

Opt-in per-band worker counts (index = QosBand). All zero (the default) = disabled: worker_count identical workers service all bands. When any element is non-zero, band k gets band_worker_counts[k] workers at band_task_priorities[k], each servicing bands 0..k. The deepest configured band’s workers service ALL bands (with a warning if that band is not Low), so every band is always reachable even with aging_threshold == 0.

std::array<std::size_t, kNumBands> band_task_priorities{10, 7, 5, 1}

espp::Task priorities for per-band workers (only used when band_worker_counts is set). Defaults descend from Critical to Low. On ESP these are FreeRTOS priorities and are always applied; on host platforms (Linux/macOS) they map to SCHED_FIFO real-time priorities (see espp::Task::BaseConfig::priority) and are only applied when band_workers_realtime is set.

bool band_workers_realtime = false

Opt-in for OS real-time scheduling of per-band workers on HOST platforms (sets espp::Task::BaseConfig::host_realtime on each worker). When false (the default), host per-band workers run at default (non-realtime) scheduling: band ordering is still honored at the queue level (workers pop the most urgent band first), but the OS scheduler does not preempt in favor of the more urgent bands’ workers. When true, band_task_priorities are applied as SCHED_FIFO real-time priorities.

Warning

SCHED_FIFO workers can starve the rest of the system if a job spins; on Linux this additionally requires CAP_SYS_NICE or an RLIMIT_RTPRIO allowance (e.g. under PREEMPT_RT), otherwise the Task falls back to default scheduling with a one-time warning. Ignored on ESP, where FreeRTOS priorities are always applied.

espp::Logger::Verbosity log_level = espp::Logger::Verbosity::WARN

Logger verbosity level.

struct Stats

Snapshot of pool activity counters.

Public Members

std::uint64_t submitted = 0

Total jobs accepted into the queue.

std::uint64_t executed = 0

Total jobs executed (counted when a worker begins executing the job, so a job’s own side effects always observe it).

std::uint64_t rejected = 0

full) or dropped (due to stop, the enqueued jobs were dropped).

Total jobs rejected (invalid job, stopped/stopping, or queue

std::array<std::uint64_t, kNumBands> band_submitted = {}

Jobs accepted per band (by the band they were submitted to).

std::array<std::uint64_t, kNumBands> band_executed = {}

Jobs executed per band (by the band they were popped from, i.e. after any aging promotions).

std::array<std::uint64_t, kNumBands> band_aged = {}

Aging promotions OUT of each band (an entry moved from band i to band i-1).

std::array<std::uint64_t, kNumBands> band_rejected = {}

Jobs rejected per band (by the band they were submitted to; on stop(), dropped queued jobs count against the band they were queued in).

Header File