From 32ff75e3556d792b8ddf0982fb6775c8e80a1e0e Mon Sep 17 00:00:00 2001 From: yhirose Date: Wed, 1 Jul 2026 22:08:01 -0400 Subject: [PATCH] Make ThreadPool idle timeout configurable at runtime (Fix #2481) --- README.md | 9 +++++++++ httplib.h | 17 +++++++++++------ test/test.cc | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index bc9c8fe..5f53992 100644 --- a/README.md +++ b/README.md @@ -824,6 +824,15 @@ svr.new_task_queue = [] { return new ThreadPool(/*base_threads=*/12, /*max_threa Default limit is 0 (unlimited). Once the limit is reached, the listener will shutdown the client connection. +#### Idle timeout for dynamic threads + +The idle timeout for dynamic threads can also be set at runtime via the +fourth parameter (in seconds): + +```cpp +svr.new_task_queue = [] { return new ThreadPool(/*base_threads=*/8, /*max_threads=*/64, /*max_queued_requests=*/0, /*idle_timeout_sec=*/10); }; +``` + ### Override the default thread pool with yours You can supply your own thread pool implementation according to your need. diff --git a/httplib.h b/httplib.h index d30f0d6..439d915 100644 --- a/httplib.h +++ b/httplib.h @@ -1541,7 +1541,9 @@ public: class ThreadPool final : public TaskQueue { public: - explicit ThreadPool(size_t n, size_t max_n = 0, size_t mqr = 0); + explicit ThreadPool( + size_t n, size_t max_n = 0, size_t mqr = 0, + time_t idle_timeout_sec = CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT); ThreadPool(const ThreadPool &) = delete; ~ThreadPool() override = default; @@ -1556,6 +1558,7 @@ private: size_t base_thread_count_; size_t max_thread_count_; size_t max_queued_requests_; + time_t idle_timeout_sec_; size_t idle_thread_count_; bool shutdown_; @@ -10304,8 +10307,10 @@ inline ssize_t detail::BodyReader::read(char *buf, size_t len) { } // ThreadPool implementation -inline ThreadPool::ThreadPool(size_t n, size_t max_n, size_t mqr) - : base_thread_count_(n), max_queued_requests_(mqr), idle_thread_count_(0), +inline ThreadPool::ThreadPool(size_t n, size_t max_n, size_t mqr, + time_t idle_timeout_sec) + : base_thread_count_(n), max_queued_requests_(mqr), + idle_timeout_sec_(idle_timeout_sec), idle_thread_count_(0), shutdown_(false) { #ifndef CPPHTTPLIB_NO_EXCEPTIONS if (max_n != 0 && max_n < n) { @@ -10415,9 +10420,9 @@ inline void ThreadPool::worker(bool is_dynamic) { idle_thread_count_++; if (is_dynamic) { - auto has_work = cond_.wait_for( - lock, std::chrono::seconds(CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT), - [&] { return !jobs_.empty() || shutdown_; }); + auto has_work = + cond_.wait_for(lock, std::chrono::seconds(idle_timeout_sec_), + [&] { return !jobs_.empty() || shutdown_; }); if (!has_work) { // Timed out with no work - exit this dynamic thread idle_thread_count_--; diff --git a/test/test.cc b/test/test.cc index d3f8e25..9fd015a 100644 --- a/test/test.cc +++ b/test/test.cc @@ -13476,6 +13476,44 @@ TEST(TaskQueueTest, IncreaseAtomicIntegerWithQueueLimit) { EXPECT_TRUE(queued_count >= qlimit); } +TEST(TaskQueueTest, IdleTimeoutAtRuntime) { + // Use a short idle timeout so a dynamic thread spawns, times out and + // exits during the test, and the pool keeps working afterwards. + std::unique_ptr task_queue{new ThreadPool{ + /*num_threads=*/1, /*max_threads=*/2, /*max_queued_requests=*/0, + /*idle_timeout_sec=*/1}}; + + std::atomic_uint count{0}; + std::condition_variable cv; + std::mutex mtx; + bool release = false; + + // Block the base thread so the second task spawns a dynamic thread. + EXPECT_TRUE(task_queue->enqueue([&] { + std::unique_lock lock(mtx); + while (!release) { + cv.wait(lock); + } + count++; + })); + EXPECT_TRUE(task_queue->enqueue([&] { count++; })); + + // Let the dynamic thread finish its task and exceed the idle timeout. + std::this_thread::sleep_for(std::chrono::milliseconds(1500)); + + { + std::unique_lock lock(mtx); + release = true; + } + cv.notify_all(); + + // The pool must still accept and run tasks after the dynamic thread exited. + EXPECT_TRUE(task_queue->enqueue([&] { count++; })); + + task_queue->shutdown(); + EXPECT_EQ(3u, count.load()); +} + TEST(TaskQueueTest, MaxQueuedRequests) { static constexpr unsigned int qlimit{3}; std::unique_ptr task_queue{new ThreadPool{1, 1, qlimit}};