Make ThreadPool ctor exception-safe on partial thread creation (#2445)

* Make ThreadPool ctor exception-safe on partial thread creation

If std::thread construction throws partway through the ThreadPool
constructor (e.g., pthread_create returns EAGAIN under thread-resource
pressure), the partially-built threads_ vector would destruct joinable
std::thread objects, calling std::terminate(). Wrap the spawn loop and,
on failure, signal shutdown to the workers already created, join them,
and rethrow.

Adds a reproducer test in test_thread_pool.cc that interposes
pthread_create at link time to deterministically fail the second call,
gated to POSIX + exceptions-enabled builds.

Fix #2444

* Strip ASAN from test_thread_pool to coexist with pthread_create override

Linux libasan installs its own pthread_create interceptor; our in-binary
symbol override sits on top of it and corrupts ASAN's thread bookkeeping,
which surfaces as "Joining already joined thread" on the very first test.
Disable ASAN for this small unit-test binary -- ThreadPool memory behavior
is still exercised under ASAN by the main `test` binary.
This commit is contained in:
yhirose
2026-05-09 21:13:40 -04:00
committed by GitHub
parent 600d220c84
commit 7d5082cc0e
3 changed files with 97 additions and 3 deletions

View File

@@ -198,6 +198,63 @@ TEST(ThreadPoolTest, InvalidMaxThreadsThrows) {
}
#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();