Skip to content

Commit 32ff75e

Browse files
committed
Make ThreadPool idle timeout configurable at runtime (Fix #2481)
1 parent 45da614 commit 32ff75e

3 files changed

Lines changed: 58 additions & 6 deletions

File tree

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,15 @@ svr.new_task_queue = [] { return new ThreadPool(/*base_threads=*/12, /*max_threa
824824
Default limit is 0 (unlimited). Once the limit is reached, the listener
825825
will shutdown the client connection.
826826

827+
#### Idle timeout for dynamic threads
828+
829+
The idle timeout for dynamic threads can also be set at runtime via the
830+
fourth parameter (in seconds):
831+
832+
```cpp
833+
svr.new_task_queue = [] { return new ThreadPool(/*base_threads=*/8, /*max_threads=*/64, /*max_queued_requests=*/0, /*idle_timeout_sec=*/10); };
834+
```
835+
827836
### Override the default thread pool with yours
828837
829838
You can supply your own thread pool implementation according to your need.

httplib.h

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1541,7 +1541,9 @@ class TaskQueue {
15411541

15421542
class ThreadPool final : public TaskQueue {
15431543
public:
1544-
explicit ThreadPool(size_t n, size_t max_n = 0, size_t mqr = 0);
1544+
explicit ThreadPool(
1545+
size_t n, size_t max_n = 0, size_t mqr = 0,
1546+
time_t idle_timeout_sec = CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT);
15451547
ThreadPool(const ThreadPool &) = delete;
15461548
~ThreadPool() override = default;
15471549

@@ -1556,6 +1558,7 @@ class ThreadPool final : public TaskQueue {
15561558
size_t base_thread_count_;
15571559
size_t max_thread_count_;
15581560
size_t max_queued_requests_;
1561+
time_t idle_timeout_sec_;
15591562
size_t idle_thread_count_;
15601563

15611564
bool shutdown_;
@@ -10304,8 +10307,10 @@ inline ssize_t detail::BodyReader::read(char *buf, size_t len) {
1030410307
}
1030510308

1030610309
// ThreadPool implementation
10307-
inline ThreadPool::ThreadPool(size_t n, size_t max_n, size_t mqr)
10308-
: base_thread_count_(n), max_queued_requests_(mqr), idle_thread_count_(0),
10310+
inline ThreadPool::ThreadPool(size_t n, size_t max_n, size_t mqr,
10311+
time_t idle_timeout_sec)
10312+
: base_thread_count_(n), max_queued_requests_(mqr),
10313+
idle_timeout_sec_(idle_timeout_sec), idle_thread_count_(0),
1030910314
shutdown_(false) {
1031010315
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
1031110316
if (max_n != 0 && max_n < n) {
@@ -10415,9 +10420,9 @@ inline void ThreadPool::worker(bool is_dynamic) {
1041510420
idle_thread_count_++;
1041610421

1041710422
if (is_dynamic) {
10418-
auto has_work = cond_.wait_for(
10419-
lock, std::chrono::seconds(CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT),
10420-
[&] { return !jobs_.empty() || shutdown_; });
10423+
auto has_work =
10424+
cond_.wait_for(lock, std::chrono::seconds(idle_timeout_sec_),
10425+
[&] { return !jobs_.empty() || shutdown_; });
1042110426
if (!has_work) {
1042210427
// Timed out with no work - exit this dynamic thread
1042310428
idle_thread_count_--;

test/test.cc

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13476,6 +13476,44 @@ TEST(TaskQueueTest, IncreaseAtomicIntegerWithQueueLimit) {
1347613476
EXPECT_TRUE(queued_count >= qlimit);
1347713477
}
1347813478

13479+
TEST(TaskQueueTest, IdleTimeoutAtRuntime) {
13480+
// Use a short idle timeout so a dynamic thread spawns, times out and
13481+
// exits during the test, and the pool keeps working afterwards.
13482+
std::unique_ptr<TaskQueue> task_queue{new ThreadPool{
13483+
/*num_threads=*/1, /*max_threads=*/2, /*max_queued_requests=*/0,
13484+
/*idle_timeout_sec=*/1}};
13485+
13486+
std::atomic_uint count{0};
13487+
std::condition_variable cv;
13488+
std::mutex mtx;
13489+
bool release = false;
13490+
13491+
// Block the base thread so the second task spawns a dynamic thread.
13492+
EXPECT_TRUE(task_queue->enqueue([&] {
13493+
std::unique_lock<std::mutex> lock(mtx);
13494+
while (!release) {
13495+
cv.wait(lock);
13496+
}
13497+
count++;
13498+
}));
13499+
EXPECT_TRUE(task_queue->enqueue([&] { count++; }));
13500+
13501+
// Let the dynamic thread finish its task and exceed the idle timeout.
13502+
std::this_thread::sleep_for(std::chrono::milliseconds(1500));
13503+
13504+
{
13505+
std::unique_lock<std::mutex> lock(mtx);
13506+
release = true;
13507+
}
13508+
cv.notify_all();
13509+
13510+
// The pool must still accept and run tasks after the dynamic thread exited.
13511+
EXPECT_TRUE(task_queue->enqueue([&] { count++; }));
13512+
13513+
task_queue->shutdown();
13514+
EXPECT_EQ(3u, count.load());
13515+
}
13516+
1347913517
TEST(TaskQueueTest, MaxQueuedRequests) {
1348013518
static constexpr unsigned int qlimit{3};
1348113519
std::unique_ptr<TaskQueue> task_queue{new ThreadPool{1, 1, qlimit}};

0 commit comments

Comments
 (0)