| // ThreadPool unit tests | |
| // Set a short idle timeout for faster shrink tests | |
| #define CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT 1 | |
| #include <httplib.h> | |
| #include <gtest/gtest.h> | |
| #include <atomic> | |
| #include <chrono> | |
| #include <thread> | |
| #include <vector> | |
| using namespace httplib; | |
| TEST(ThreadPoolTest, BasicTaskExecution) { | |
| ThreadPool pool(4); | |
| std::atomic<int> count(0); | |
| for (int i = 0; i < 10; i++) { | |
| pool.enqueue([&count]() { count++; }); | |
| } | |
| pool.shutdown(); | |
| EXPECT_EQ(10, count.load()); | |
| } | |
| TEST(ThreadPoolTest, FixedPoolWhenMaxEqualsBase) { | |
| // max_n == 0 means max = base (fixed pool behavior) | |
| ThreadPool pool(4); | |
| std::atomic<int> count(0); | |
| for (int i = 0; i < 100; i++) { | |
| pool.enqueue([&count]() { count++; }); | |
| } | |
| pool.shutdown(); | |
| EXPECT_EQ(100, count.load()); | |
| } | |
| TEST(ThreadPoolTest, DynamicScaleUp) { | |
| // base=2, max=8: block 2 base threads, then enqueue more tasks | |
| ThreadPool pool(2, 8); | |
| std::atomic<int> active(0); | |
| std::atomic<int> max_active(0); | |
| std::atomic<int> completed(0); | |
| std::mutex barrier_mutex; | |
| std::condition_variable barrier_cv; | |
| bool release = false; | |
| // Occupy all base threads with blocking tasks | |
| for (int i = 0; i < 2; i++) { | |
| pool.enqueue([&]() { | |
| active++; | |
| { | |
| std::unique_lock<std::mutex> lock(barrier_mutex); | |
| barrier_cv.wait(lock, [&] { return release; }); | |
| } | |
| active--; | |
| completed++; | |
| }); | |
| } | |
| // Wait for base threads to be occupied | |
| std::this_thread::sleep_for(std::chrono::milliseconds(100)); | |
| // These should trigger dynamic thread creation | |
| for (int i = 0; i < 4; i++) { | |
| pool.enqueue([&]() { | |
| int cur = ++active; | |
| // Track peak active count | |
| int prev = max_active.load(); | |
| while (cur > prev && !max_active.compare_exchange_weak(prev, cur)) {} | |
| std::this_thread::sleep_for(std::chrono::milliseconds(50)); | |
| active--; | |
| completed++; | |
| }); | |
| } | |
| // Wait for dynamic tasks to complete | |
| std::this_thread::sleep_for(std::chrono::milliseconds(500)); | |
| // Release the blocking tasks | |
| { | |
| std::unique_lock<std::mutex> lock(barrier_mutex); | |
| release = true; | |
| } | |
| barrier_cv.notify_all(); | |
| pool.shutdown(); | |
| EXPECT_EQ(6, completed.load()); | |
| // More than 2 threads were active simultaneously | |
| EXPECT_GT(max_active.load(), 2); | |
| } | |
| TEST(ThreadPoolTest, DynamicShrinkAfterIdle) { | |
| // CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT is set to 1 second | |
| ThreadPool pool(2, 8); | |
| std::atomic<int> completed(0); | |
| // Enqueue tasks that require dynamic threads | |
| for (int i = 0; i < 8; i++) { | |
| pool.enqueue([&]() { | |
| std::this_thread::sleep_for(std::chrono::milliseconds(100)); | |
| completed++; | |
| }); | |
| } | |
| // Wait for all tasks to complete + idle timeout + margin | |
| std::this_thread::sleep_for(std::chrono::milliseconds(2500)); | |
| // Now enqueue a simple task to verify the pool still works | |
| // (base threads are still alive) | |
| std::atomic<bool> final_task_done(false); | |
| pool.enqueue([&]() { final_task_done = true; }); | |
| std::this_thread::sleep_for(std::chrono::milliseconds(100)); | |
| pool.shutdown(); | |
| EXPECT_EQ(8, completed.load()); | |
| EXPECT_TRUE(final_task_done.load()); | |
| } | |
| TEST(ThreadPoolTest, ShutdownWithActiveDynamicThreads) { | |
| ThreadPool pool(2, 8); | |
| std::atomic<int> started(0); | |
| std::mutex block_mutex; | |
| std::condition_variable block_cv; | |
| bool release = false; | |
| // Start tasks on dynamic threads that block until released | |
| for (int i = 0; i < 6; i++) { | |
| pool.enqueue([&]() { | |
| started++; | |
| std::unique_lock<std::mutex> lock(block_mutex); | |
| block_cv.wait(lock, [&] { return release; }); | |
| }); | |
| } | |
| // Wait for tasks to start | |
| std::this_thread::sleep_for(std::chrono::milliseconds(200)); | |
| EXPECT_GE(started.load(), 2); | |
| // Release all blocked threads, then shutdown | |
| { | |
| std::unique_lock<std::mutex> lock(block_mutex); | |
| release = true; | |
| } | |
| block_cv.notify_all(); | |
| pool.shutdown(); | |
| } | |
| TEST(ThreadPoolTest, MaxQueuedRequests) { | |
| // base=2, max=2 (fixed), mqr=3 | |
| ThreadPool pool(2, 2, 3); | |
| std::mutex block_mutex; | |
| std::condition_variable block_cv; | |
| bool release = false; | |
| // Block both threads | |
| for (int i = 0; i < 2; i++) { | |
| EXPECT_TRUE(pool.enqueue([&]() { | |
| std::unique_lock<std::mutex> lock(block_mutex); | |
| block_cv.wait(lock, [&] { return release; }); | |
| })); | |
| } | |
| std::this_thread::sleep_for(std::chrono::milliseconds(100)); | |
| // Fill the queue up to max_queued_requests | |
| EXPECT_TRUE(pool.enqueue([]() {})); | |
| EXPECT_TRUE(pool.enqueue([]() {})); | |
| EXPECT_TRUE(pool.enqueue([]() {})); | |
| // This should fail - queue is full | |
| EXPECT_FALSE(pool.enqueue([]() {})); | |
| // Release blocked threads | |
| { | |
| std::unique_lock<std::mutex> lock(block_mutex); | |
| release = true; | |
| } | |
| block_cv.notify_all(); | |
| pool.shutdown(); | |
| } | |
| #ifndef CPPHTTPLIB_NO_EXCEPTIONS | |
| TEST(ThreadPoolTest, InvalidMaxThreadsThrows) { | |
| // max_n < n should throw | |
| EXPECT_THROW(ThreadPool(8, 4), std::invalid_argument); | |
| } | |
| #endif | |
| // Issue #2444: ThreadPool constructor must be exception-safe when std::thread | |
| // construction fails partway (e.g., pthread_create returns EAGAIN under thread | |
| // resource pressure). Without proper handling, the partially-built threads_ | |
| // vector destroys joinable std::thread objects, calling std::terminate(). | |
| // | |
| // We reproduce the failure portably by interposing pthread_create at link | |
| // time: while the counter is armed, the first N calls succeed, the rest | |
| // return EAGAIN. This is gated to POSIX + exceptions-enabled builds. | |
| #ifndef CPPHTTPLIB_NO_EXCEPTIONS | |
| #if defined(__unix__) || defined(__APPLE__) | |
| #include <dlfcn.h> | |
| #include <errno.h> | |
| #include <pthread.h> | |
| namespace { | |
| // -1 = pass-through (default). >= 0 = number of remaining successful calls | |
| // before EAGAIN is returned. Reset to -1 after each test that arms it. | |
| std::atomic<int> g_pthread_create_remaining{-1}; | |
| } // namespace | |
| extern "C" int pthread_create(pthread_t *thread, const pthread_attr_t *attr, | |
| void *(*start_routine)(void *), void *arg) { | |
| using fn_t = | |
| int (*)(pthread_t *, const pthread_attr_t *, void *(*)(void *), void *); | |
| static fn_t real = reinterpret_cast<fn_t>(dlsym(RTLD_NEXT, "pthread_create")); | |
| int n = g_pthread_create_remaining.load(std::memory_order_relaxed); | |
| if (n == 0) { return EAGAIN; } | |
| if (n > 0) { | |
| g_pthread_create_remaining.fetch_sub(1, std::memory_order_relaxed); | |
| } | |
| return real(thread, attr, start_routine, arg); | |
| } | |
| TEST(ThreadPoolTest, ConstructorRecoversWhenThreadCreationFails) { | |
| // Allow only the first thread to spawn; subsequent pthread_create calls | |
| // return EAGAIN, causing std::thread() to throw std::system_error mid-loop. | |
| g_pthread_create_remaining.store(1); | |
| bool caught = false; | |
| try { | |
| ThreadPool pool(/*n=*/4); | |
| (void)pool; | |
| } catch (const std::system_error &) { caught = true; } catch (...) { | |
| caught = true; | |
| } | |
| // Disarm before any further test runs. | |
| g_pthread_create_remaining.store(-1); | |
| EXPECT_TRUE(caught); | |
| } | |
| #endif // POSIX | |
| #endif // CPPHTTPLIB_NO_EXCEPTIONS | |
| TEST(ThreadPoolTest, EnqueueAfterShutdownReturnsFalse) { | |
| ThreadPool pool(2); | |
| pool.shutdown(); | |
| EXPECT_FALSE(pool.enqueue([]() {})); | |
| } | |
| TEST(ThreadPoolTest, ConcurrentEnqueue) { | |
| ThreadPool pool(4, 16); | |
| std::atomic<int> count(0); | |
| const int num_producers = 4; | |
| const int tasks_per_producer = 100; | |
| std::vector<std::thread> producers; | |
| for (int p = 0; p < num_producers; p++) { | |
| producers.emplace_back([&]() { | |
| for (int i = 0; i < tasks_per_producer; i++) { | |
| pool.enqueue([&count]() { count++; }); | |
| } | |
| }); | |
| } | |
| for (auto &t : producers) { | |
| t.join(); | |
| } | |
| pool.shutdown(); | |
| EXPECT_EQ(num_producers * tasks_per_producer, count.load()); | |
| } |