From 7502be3b0e09479b7cd88bc8d751143b8794f40e Mon Sep 17 00:00:00 2001 From: Luke Street Date: Thu, 23 Oct 2025 11:13:39 -0600 Subject: [PATCH 1/2] Add thread pool async I/O backend --- CMakeLists.txt | 1 + src/async_io.cpp | 39 +++++-- src/async_io.h | 1 + src/async_io_threadpool.cpp | 214 ++++++++++++++++++++++++++++++++++++ 4 files changed, 248 insertions(+), 7 deletions(-) create mode 100644 src/async_io_threadpool.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e94d63..e180138 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -164,6 +164,7 @@ add_executable(wibo dll/version.cpp src/access.cpp src/async_io.cpp + src/async_io_threadpool.cpp src/context.cpp src/errors.cpp src/files.cpp diff --git a/src/async_io.cpp b/src/async_io.cpp index 1de8ea5..02b4e3b 100644 --- a/src/async_io.cpp +++ b/src/async_io.cpp @@ -38,18 +38,43 @@ class NoOpBackend : public wibo::AsyncIOBackend { namespace wibo { -AsyncIOBackend &asyncIO() { - if (!g_backend) { +using BackendFactory = auto (*)() -> std::unique_ptr; + +struct BackendEntry { + const char *name; + BackendFactory factory; +}; + +static constexpr BackendEntry kBackends[] = { #if WIBO_ENABLE_LIBURING - g_backend = detail::createIoUringBackend(); -#else - g_backend = std::make_unique(); + {"io_uring", detail::createIoUringBackend}, #endif + {"thread pool", detail::createThreadPoolBackend}, +}; + +AsyncIOBackend &asyncIO() { + if (!g_backend) { + for (const auto &entry : kBackends) { + DEBUG_LOG("AsyncIO: initializing %s backend\n", entry.name); + auto backend = entry.factory(); + if (backend && backend->init()) { + g_backend = std::move(backend); + break; + } else { + DEBUG_LOG("AsyncIO: %s backend unavailable\n", entry.name); + if (backend) { + backend->shutdown(); + } + } + } } - if (!g_backend->init()) { - DEBUG_LOG("AsyncIOBackend initialization failed; using no-op backend\n"); + + if (!g_backend) { + DEBUG_LOG("AsyncIO: no backend available; using no-op backend\n"); g_backend = std::make_unique(); + g_backend->init(); } + return *g_backend; } diff --git a/src/async_io.h b/src/async_io.h index 4674b37..4c52fd1 100644 --- a/src/async_io.h +++ b/src/async_io.h @@ -24,6 +24,7 @@ namespace detail { #if WIBO_ENABLE_LIBURING std::unique_ptr createIoUringBackend(); #endif +std::unique_ptr createThreadPoolBackend(); } // namespace detail diff --git a/src/async_io_threadpool.cpp b/src/async_io_threadpool.cpp new file mode 100644 index 0000000..43782aa --- /dev/null +++ b/src/async_io_threadpool.cpp @@ -0,0 +1,214 @@ +#include "async_io.h" + +#include "errors.h" +#include "files.h" +#include "kernel32/overlapped_util.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct AsyncRequest { + enum class Kind { Read, Write }; + + Kind kind; + Pin file; + OVERLAPPED *overlapped = nullptr; + void *buffer = nullptr; + DWORD length = 0; + std::optional offset; + bool isPipe = false; + bool updateFilePointer = false; + + explicit AsyncRequest(Kind k) : kind(k) {} +}; + +class ThreadPoolBackend : public wibo::AsyncIOBackend { + public: + ~ThreadPoolBackend() override { shutdown(); } + + bool init() override; + void shutdown() override; + [[nodiscard]] bool running() const noexcept override { return mActive.load(std::memory_order_acquire); } + + bool queueRead(Pin file, OVERLAPPED *ov, void *buffer, DWORD length, + const std::optional &offset, bool isPipe) override; + bool queueWrite(Pin file, OVERLAPPED *ov, const void *buffer, DWORD length, + const std::optional &offset, bool isPipe) override; + + private: + bool enqueueRequest(std::unique_ptr req); + void workerLoop(); + static void processRequest(const AsyncRequest &req); + + std::atomic mActive{false}; + std::mutex mQueueMutex; + std::condition_variable mQueueCv; + std::deque> mQueue; + std::vector mWorkers; + std::atomic mPending{0}; + bool mStopping = false; // guarded by mQueueMutex +}; + +bool ThreadPoolBackend::init() { + bool expected = false; + if (!mActive.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) { + return true; + } + + unsigned int threadCount = std::thread::hardware_concurrency(); + if (threadCount == 0) { + threadCount = 1; + } + threadCount = std::min(threadCount, 4u); // cap to avoid oversubscription + + { + std::lock_guard lk(mQueueMutex); + mStopping = false; + } + mWorkers.reserve(threadCount); + for (unsigned int i = 0; i < threadCount; ++i) { + mWorkers.emplace_back(&ThreadPoolBackend::workerLoop, this); + } + DEBUG_LOG("thread-pool async backend initialized (%u worker%s)\n", threadCount, threadCount == 1 ? "" : "s"); + return true; +} + +void ThreadPoolBackend::shutdown() { + if (!mActive.exchange(false, std::memory_order_acq_rel)) { + return; + } + + { + std::lock_guard lk(mQueueMutex); + mStopping = true; + } + mQueueCv.notify_all(); + + for (auto &worker : mWorkers) { + if (worker.joinable()) { + worker.join(); + } + } + mWorkers.clear(); + + { + std::lock_guard lk(mQueueMutex); + mQueue.clear(); + mStopping = false; + } + mPending.store(0, std::memory_order_release); + DEBUG_LOG("thread-pool async backend shut down\n"); +} + +bool ThreadPoolBackend::queueRead(Pin file, OVERLAPPED *ov, void *buffer, DWORD length, + const std::optional &offset, bool isPipe) { + auto req = std::make_unique(AsyncRequest::Kind::Read); + req->file = std::move(file); + req->overlapped = ov; + req->buffer = buffer; + req->length = length; + req->offset = offset; + req->isPipe = isPipe; + req->updateFilePointer = req->file ? !req->file->overlapped : true; + return enqueueRequest(std::move(req)); +} + +bool ThreadPoolBackend::queueWrite(Pin file, OVERLAPPED *ov, const void *buffer, DWORD length, + const std::optional &offset, bool isPipe) { + auto req = std::make_unique(AsyncRequest::Kind::Write); + req->file = std::move(file); + req->overlapped = ov; + req->buffer = const_cast(buffer); + req->length = length; + req->offset = offset; + req->isPipe = isPipe; + req->updateFilePointer = req->file ? !req->file->overlapped : true; + return enqueueRequest(std::move(req)); +} + +bool ThreadPoolBackend::enqueueRequest(std::unique_ptr req) { + if (!running()) { + return false; + } + if (!req || !req->file) { + return false; + } + + { + std::lock_guard lk(mQueueMutex); + if (mStopping) { + return false; + } + mQueue.emplace_back(std::move(req)); + mPending.fetch_add(1, std::memory_order_acq_rel); + } + mQueueCv.notify_one(); + return true; +} + +void ThreadPoolBackend::workerLoop() { + while (true) { + std::unique_ptr req; + { + std::unique_lock lk(mQueueMutex); + mQueueCv.wait(lk, [&] { return mStopping || !mQueue.empty(); }); + if (mStopping && mQueue.empty()) { + break; + } + req = std::move(mQueue.front()); + mQueue.pop_front(); + } + + if (req) { + processRequest(*req); + } + mPending.fetch_sub(1, std::memory_order_acq_rel); + } +} + +void ThreadPoolBackend::processRequest(const AsyncRequest &req) { + if (!req.file || !req.file->valid()) { + kernel32::detail::signalOverlappedEvent(req.file.get(), req.overlapped, STATUS_INVALID_HANDLE, 0); + return; + } + + files::IOResult io{}; + if (req.kind == AsyncRequest::Kind::Read) { + io = files::read(req.file.get(), req.buffer, req.length, req.offset, req.updateFilePointer); + } else { + const void *ptr = req.buffer; + io = files::write(req.file.get(), ptr, req.length, req.offset, req.updateFilePointer); + } + + NTSTATUS completionStatus = STATUS_SUCCESS; + size_t bytesTransferred = io.bytesTransferred; + + if (io.unixError != 0) { + completionStatus = wibo::statusFromErrno(io.unixError); + if (completionStatus == STATUS_SUCCESS) { + completionStatus = STATUS_UNEXPECTED_IO_ERROR; + } + } else if (req.kind == AsyncRequest::Kind::Read && bytesTransferred == 0 && io.reachedEnd) { + completionStatus = req.isPipe ? STATUS_PIPE_BROKEN : STATUS_END_OF_FILE; + } else if (req.kind == AsyncRequest::Kind::Write && bytesTransferred == 0 && io.reachedEnd) { + completionStatus = STATUS_END_OF_FILE; + } + + kernel32::detail::signalOverlappedEvent(req.file.get(), req.overlapped, completionStatus, bytesTransferred); +} + +} // namespace + +namespace wibo::detail { + +std::unique_ptr createThreadPoolBackend() { return std::make_unique(); } + +} // namespace wibo::detail From 1a75143756f66fb817d68337ad71d4a91dbc59bb Mon Sep 17 00:00:00 2001 From: Luke Street Date: Thu, 23 Oct 2025 11:22:45 -0600 Subject: [PATCH 2/2] Make it work with older code --- src/async_io_threadpool.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/async_io_threadpool.cpp b/src/async_io_threadpool.cpp index 43782aa..c427a7e 100644 --- a/src/async_io_threadpool.cpp +++ b/src/async_io_threadpool.cpp @@ -77,7 +77,7 @@ bool ThreadPoolBackend::init() { for (unsigned int i = 0; i < threadCount; ++i) { mWorkers.emplace_back(&ThreadPoolBackend::workerLoop, this); } - DEBUG_LOG("thread-pool async backend initialized (%u worker%s)\n", threadCount, threadCount == 1 ? "" : "s"); + DEBUG_LOG("thread pool backend initialized (workers=%u)\n", threadCount); return true; } @@ -176,7 +176,11 @@ void ThreadPoolBackend::workerLoop() { void ThreadPoolBackend::processRequest(const AsyncRequest &req) { if (!req.file || !req.file->valid()) { - kernel32::detail::signalOverlappedEvent(req.file.get(), req.overlapped, STATUS_INVALID_HANDLE, 0); + if (req.overlapped) { + req.overlapped->Internal = STATUS_INVALID_HANDLE; + req.overlapped->InternalHigh = 0; + kernel32::detail::signalOverlappedEvent(req.overlapped); + } return; } @@ -202,7 +206,11 @@ void ThreadPoolBackend::processRequest(const AsyncRequest &req) { completionStatus = STATUS_END_OF_FILE; } - kernel32::detail::signalOverlappedEvent(req.file.get(), req.overlapped, completionStatus, bytesTransferred); + if (req.overlapped) { + req.overlapped->Internal = completionStatus; + req.overlapped->InternalHigh = bytesTransferred; + kernel32::detail::signalOverlappedEvent(req.overlapped); + } } } // namespace