From 90cb0119e28b790e7c095593a832e27cafc181d8 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Tue, 23 Jun 2026 18:06:44 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=8F=E7=A8=8B=E8=B0=83=E5=BA=A6=E5=99=A8?= =?UTF-8?q?=EF=BC=8C=E9=87=8D=E5=A4=A7=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + 3rd/ucoro/include/ucoro/asio_glue.hpp | 206 ---- 3rd/ucoro/include/ucoro/awaitable.hpp | 883 +++++++----------- 3rd/ucoro/include/ucoro/single_thread.h | 106 +++ 3rd/ucoro/main.cmake | 0 3rd/ucoro/src/single_thread.cpp | 235 +++++ .../tests/single_thread_scheduler_tests.cpp | 656 +++++++++++++ main.cmake | 46 +- main.cpp | 10 +- 9 files changed, 1336 insertions(+), 807 deletions(-) create mode 100644 .gitignore delete mode 100644 3rd/ucoro/include/ucoro/asio_glue.hpp create mode 100644 3rd/ucoro/include/ucoro/single_thread.h create mode 100644 3rd/ucoro/main.cmake create mode 100644 3rd/ucoro/src/single_thread.cpp create mode 100644 3rd/ucoro/tests/single_thread_scheduler_tests.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7adcaac --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/3rd/ucoro.zip diff --git a/3rd/ucoro/include/ucoro/asio_glue.hpp b/3rd/ucoro/include/ucoro/asio_glue.hpp deleted file mode 100644 index a5d3dd6..0000000 --- a/3rd/ucoro/include/ucoro/asio_glue.hpp +++ /dev/null @@ -1,206 +0,0 @@ -#pragma once - -// Experimental Boost.Asio interop layer. -// This header is intentionally not part of the stable ucoro core API. -// The stable core contract is callback -> ucoro::awaitable without a scheduler. - -#include "./awaitable.hpp" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -namespace ucoro::asio_glue -{ - template - struct asio_awaitable_state - { - explicit asio_awaitable_state(boost::asio::awaitable&& awaitable) - : asio_awaitable(std::move(awaitable)) - { - } - - boost::asio::awaitable asio_awaitable; - std::optional value; - std::exception_ptr exception; - std::coroutine_handle<> continuation; - }; - - template<> - struct asio_awaitable_state - { - explicit asio_awaitable_state(boost::asio::awaitable&& awaitable) - : asio_awaitable(std::move(awaitable)) - { - } - - boost::asio::awaitable asio_awaitable; - std::exception_ptr exception; - std::coroutine_handle<> continuation; - }; - - template - struct asio_awaitable_awaiter - { - explicit asio_awaitable_awaiter(boost::asio::awaitable&& asio_awaitable) - : state_(std::make_shared>(std::move(asio_awaitable))) - { - } - - constexpr bool await_ready() const noexcept - { - return false; - } - - template - void await_suspend(std::coroutine_handle continue_handle) - { - boost::asio::any_io_executor executor; - - if constexpr (ucoro::concepts::awaitable_promise_type) - { - if (continue_handle.promise().local_) - { - try - { - executor = std::any_cast(*continue_handle.promise().local_); - } - catch (const std::bad_any_cast&) - { - std::terminate(); - } - } - else - { - std::terminate(); - } - } - else - { - std::terminate(); - } - - state_->continuation = continue_handle; - auto state = state_; - - boost::asio::co_spawn(executor, - [state]() mutable -> boost::asio::awaitable - { - try - { - if constexpr (std::is_void_v) - { - co_await std::move(state->asio_awaitable); - } - else - { - state->value.emplace(co_await std::move(state->asio_awaitable)); - } - } - catch (...) - { - state->exception = std::current_exception(); - } - - state->continuation.resume(); - co_return; - }, - [](std::exception_ptr) {}); - } - - T await_resume() - { - if (state_->exception) - { - std::rethrow_exception(state_->exception); - } - - if constexpr (std::is_void_v) - { - return; - } - else - { - return std::move(*state_->value); - } - } - - std::shared_ptr> state_; - }; - - template - struct initiate_do_invoke_ucoro_awaitable - { - template - void operator()(Handler&& handler, ucoro::awaitable ucoro_awaitable) const - { - auto executor = boost::asio::get_associated_executor(handler); - - if constexpr (std::is_void_v) - { - auto task = [handler = std::move(handler), ucoro_awaitable = std::move(ucoro_awaitable)]() mutable -> ucoro::awaitable - { - try - { - co_await std::move(ucoro_awaitable); - handler(boost::system::error_code{}); - } - catch (...) - { - handler(boost::asio::error::operation_aborted); - } - }().detach(executor); - - task.start_detached(); - } - else - { - auto task = [handler = std::move(handler), ucoro_awaitable = std::move(ucoro_awaitable)]() mutable -> ucoro::awaitable - { - try - { - auto return_value = co_await std::move(ucoro_awaitable); - handler(boost::system::error_code{}, std::move(return_value)); - } - catch (...) - { - handler(boost::asio::error::operation_aborted, T{}); - } - }().detach(executor); - - task.start_detached(); - } - } - }; - - template - auto to_asio_awaitable(ucoro::awaitable&& ucoro_awaitable) - { - return boost::asio::async_initiate( - initiate_do_invoke_ucoro_awaitable{}, boost::asio::use_awaitable, std::move(ucoro_awaitable)); - } - - template<> - inline auto to_asio_awaitable(ucoro::awaitable&& ucoro_awaitable) - { - return boost::asio::async_initiate( - initiate_do_invoke_ucoro_awaitable{}, boost::asio::use_awaitable, std::move(ucoro_awaitable)); - } - -} // namespace ucoro::asio_glue - -template -struct ucoro::await_transformer> -{ - static auto await_transform(boost::asio::awaitable&& asio_awaitable) - { - return ucoro::asio_glue::asio_awaitable_awaiter{std::move(asio_awaitable)}; - } -}; diff --git a/3rd/ucoro/include/ucoro/awaitable.hpp b/3rd/ucoro/include/ucoro/awaitable.hpp index 4a55319..4230085 100644 --- a/3rd/ucoro/include/ucoro/awaitable.hpp +++ b/3rd/ucoro/include/ucoro/awaitable.hpp @@ -1,13 +1,10 @@ // // Distributed under the Boost Software License, Version 1.0. // - #pragma once - #ifdef DISABLE_EXCEPTION #error "DISABLE_EXCEPTION is not supported by ucoro currently" #endif - #include #include #include @@ -21,14 +18,12 @@ #include #include #include - #if defined(__has_include) #if __has_include() #include #else #include -namespace std -{ +namespace std { using std::experimental::coroutine_handle; using std::experimental::coroutine_traits; using std::experimental::noop_coroutine; @@ -39,7 +34,6 @@ namespace std #else #error "Compiler version too low to support coroutine !!!" #endif - #if defined(DEBUG) || defined(_DEBUG) #if defined(ENABLE_DEBUG_CORO_LEAK) #define DEBUG_CORO_PROMISE_LEAK @@ -47,80 +41,51 @@ namespace std inline std::unordered_set debug_coro_leak; #endif #endif - -namespace ucoro -{ +namespace ucoro { template - struct await_transformer - { + struct await_transformer { }; - template struct awaitable; - template struct awaitable_promise; - template struct Callback_Awaiter; - template - struct local_storage_t - { + struct local_storage_t { }; - inline constexpr local_storage_t local_storage; - - namespace concepts - { + namespace concepts { template - struct local_storage_type_impl : std::false_type - { + struct local_storage_type_impl : std::false_type { }; - template - struct local_storage_type_impl> : std::true_type - { + struct local_storage_type_impl> : std::true_type { }; - template inline constexpr bool local_storage_type = local_storage_type_impl>::value; - template - struct awaitable_type_impl : std::false_type - { + struct awaitable_type_impl : std::false_type { }; - template - struct awaitable_type_impl> : std::true_type - { + struct awaitable_type_impl> : std::true_type { }; - template inline constexpr bool awaitable_type = awaitable_type_impl>::value; - template - struct awaitable_promise_type_impl : std::false_type - { + struct awaitable_promise_type_impl : std::false_type { }; - template - struct awaitable_promise_type_impl> : std::true_type - { + struct awaitable_promise_type_impl> : std::true_type { }; - template inline constexpr bool awaitable_promise_type = awaitable_promise_type_impl>::value; - template inline constexpr bool is_valid_await_suspend_return_value = std::is_convertible_v> || std::is_void_v || std::is_same_v; - template - struct is_awaiter_impl : std::false_type - { + struct is_awaiter_impl : std::false_type { }; - template struct is_awaiter_impl().await_ready()), @@ -129,356 +94,255 @@ namespace ucoro : std::bool_constant< std::is_same_v().await_ready()), bool> && is_valid_await_suspend_return_value().await_suspend(std::coroutine_handle<>{}) - )>> - { + )>> { }; - // MSVC 2019 and some IDE parsers can fail to evaluate the generic SFINAE // check below for ucoro core awaiters. These explicit specializations keep // the library traits stable without changing runtime behavior. template - struct is_awaiter_impl, void> : std::true_type - { + struct is_awaiter_impl, void> : std::true_type { }; - template - struct is_awaiter_impl, void> : std::true_type - { + struct is_awaiter_impl, void> : std::true_type { }; - template inline constexpr bool is_awaiter_v = is_awaiter_impl>::value; - template - struct has_operator_co_await_impl : std::false_type - { + struct has_operator_co_await_impl : std::false_type { }; - template struct has_operator_co_await_impl().operator co_await())>> - : std::bool_constant().operator co_await())>> - { + : std::bool_constant().operator co_await())>> { }; - template inline constexpr bool has_operator_co_await = has_operator_co_await_impl>::value; - template inline constexpr bool is_awaitable_v = is_awaiter_v> || awaitable_type || has_operator_co_await>; - template - struct has_user_defined_await_transformer_impl : std::false_type - { + struct has_user_defined_await_transformer_impl : std::false_type { }; - template struct has_user_defined_await_transformer_impl>::await_transform( - std::declval()))>> : std::true_type - { + std::declval()))>> : std::true_type { }; - template inline constexpr bool has_user_defined_await_transformer = has_user_defined_await_transformer_impl::value; - template - struct is_not_awaitable : std::false_type - { + struct is_not_awaitable : std::false_type { }; } // namespace concepts - - namespace traits - { + namespace traits { template typename FromTemplate> struct template_parameter_traits; - template typename ClassTemplate, typename TemplateParameter> - struct template_parameter_traits, ClassTemplate> - { + struct template_parameter_traits, ClassTemplate> { using template_parameter = TemplateParameter; }; - template typename FromTemplate> using template_parameter_of = typename template_parameter_traits< std::decay_t, FromTemplate>::template_parameter; - template using local_storage_value_type = template_parameter_of; - template using awaitable_return_type = template_parameter_of; - template - struct exception_with_result - { + struct exception_with_result { using type = std::variant; }; - template <> - struct exception_with_result - { + struct exception_with_result { using type = std::exception_ptr; }; - template using exception_with_result_t = typename exception_with_result::type; } // namespace traits - - struct debug_coro_promise - { + struct debug_coro_promise { #if defined(DEBUG_CORO_PROMISE_LEAK) - void* operator new(std::size_t size) - { + void* operator new(std::size_t size) { void* ptr = std::malloc(size); - if (!ptr) - { + if (!ptr) { throw std::bad_alloc{}; } debug_coro_leak.insert(ptr); return ptr; } - - void operator delete(void* ptr, [[maybe_unused]] std::size_t size) - { + void operator delete(void* ptr, [[maybe_unused]] std::size_t size) { debug_coro_leak.erase(ptr); std::free(ptr); } #endif }; - template - struct awaitable_promise_value - { + struct awaitable_promise_value { template - void return_value(V&& val) - { + void return_value(V&& val) { value_.template emplace(std::forward(val)); } - - void unhandled_exception() noexcept - { + void unhandled_exception() noexcept { value_.template emplace(std::current_exception()); } - - T get_value() - { - if (std::holds_alternative(value_)) - { + T get_value() { + if (std::holds_alternative(value_)) { std::rethrow_exception(std::get(value_)); } - return std::move(std::get(value_)); } - std::variant value_{std::exception_ptr{}}; }; - template <> - struct awaitable_promise_value - { + struct awaitable_promise_value { std::exception_ptr exception_{nullptr}; - - constexpr void return_void() noexcept - { + constexpr void return_void() noexcept { } - - void unhandled_exception() noexcept - { + void unhandled_exception() noexcept { exception_ = std::current_exception(); } - - void get_value() const - { - if (exception_) - { + void get_value() const { + if (exception_) { std::rethrow_exception(exception_); } } }; - - struct operation_cancelled : public std::exception - { - [[nodiscard]] const char* what() const noexcept override - { + struct operation_cancelled : public std::exception { + [[nodiscard]] const char* what() const noexcept override { return "ucoro operation cancelled"; } }; - - struct coroutine_control_block - { - void attach(std::coroutine_handle<> handle) noexcept - { + struct coroutine_control_block { + void attach(std::coroutine_handle<> handle) noexcept { std::lock_guard lock(mutex_); handle_ = handle; } - - [[nodiscard]] bool valid() const noexcept - { + [[nodiscard]] bool valid() const noexcept { std::lock_guard lock(mutex_); return static_cast(handle_) && !completed_; } - - [[nodiscard]] bool has_handle() const noexcept - { + [[nodiscard]] bool has_handle() const noexcept { std::lock_guard lock(mutex_); return static_cast(handle_); } - - [[nodiscard]] bool started() const noexcept - { + [[nodiscard]] bool started() const noexcept { std::lock_guard lock(mutex_); return started_; } - - [[nodiscard]] bool cancel_requested() const noexcept - { + [[nodiscard]] bool cancel_requested() const noexcept { std::lock_guard lock(mutex_); return cancel_requested_; } - - [[nodiscard]] std::coroutine_handle<> handle() const noexcept - { + [[nodiscard]] std::coroutine_handle<> handle() const noexcept { std::lock_guard lock(mutex_); return handle_; } - - void mark_started() noexcept - { + void mark_started() noexcept { std::lock_guard lock(mutex_); started_ = true; } - - void request_abandon() noexcept - { + void request_abandon() noexcept { std::shared_ptr child; - { std::lock_guard lock(mutex_); cancel_requested_ = true; child = child_.lock(); } - - if (child) - { + if (child) { child->request_abandon(); } } - - void set_child(std::shared_ptr child) noexcept - { + void set_child(std::shared_ptr child) noexcept { bool cancel_child = false; - { std::lock_guard lock(mutex_); child_ = child; cancel_child = cancel_requested_; } - - if (cancel_child && child) - { + if (cancel_child && child) { child->request_abandon(); } } - - void start() noexcept - { + void start() noexcept { std::coroutine_handle<> handle{}; - { std::lock_guard lock(mutex_); - - if (!handle_ || completed_ || resume_in_progress_) - { + if (!handle_ || completed_ || resume_in_progress_) { return; } - started_ = true; resume_in_progress_ = true; handle = handle_; } - handle.resume(); - - { - std::lock_guard lock(mutex_); - resume_in_progress_ = false; - } + finish_resume(); } - - void start_detached() noexcept - { + void start_detached() noexcept { std::coroutine_handle<> handle{}; - { std::lock_guard lock(mutex_); - - if (!handle_ || completed_ || resume_in_progress_) - { + if (!handle_ || completed_ || resume_in_progress_) { return; } - started_ = true; destroy_on_completion_ = true; resume_in_progress_ = true; handle = handle_; } - handle.resume(); - - { - std::lock_guard lock(mutex_); - resume_in_progress_ = false; - } + finish_resume(); } - - void resume_from_callback() noexcept - { + void resume_from_callback() noexcept { std::coroutine_handle<> handle{}; - { std::lock_guard lock(mutex_); - - if (!handle_ || completed_ || resume_in_progress_) - { + if (!handle_ || completed_ || resume_in_progress_) { return; } - started_ = true; resume_in_progress_ = true; handle = handle_; } - handle.resume(); - - { - std::lock_guard lock(mutex_); - resume_in_progress_ = false; - } + finish_resume(); } - - void reset_or_cancel() noexcept - { - std::coroutine_handle<> handle{}; - std::shared_ptr child; - + void resume_continuation(std::coroutine_handle<> handle) noexcept { + bool already_resuming = false; { std::lock_guard lock(mutex_); - - if (!handle_) - { + if (!handle || completed_) { return; } - - if (!started_ || completed_) - { - handle = handle_; - handle_ = {}; - completed_ = true; + already_resuming = resume_in_progress_; + if (!already_resuming) { + started_ = true; + resume_in_progress_ = true; } - else - { + } + + handle.resume(); + if (!already_resuming) { + finish_resume(); + } + } + void reset_or_cancel() noexcept { + std::coroutine_handle<> handle{}; + std::shared_ptr child; + { + std::lock_guard lock(mutex_); + if (!handle_) { + return; + } + if (!started_ || completed_) { + if (resume_in_progress_) { + destroy_on_completion_ = true; + } + else { + handle = handle_; + handle_ = {}; + completed_ = true; + } + } + else { // The coroutine is already running and may be suspended inside an // external callback. Do not destroy the frame here. Mark it as // cancelled and let the callback resume it once so it can unwind to @@ -488,36 +352,37 @@ namespace ucoro child = child_.lock(); } } - - if (child) - { + if (child) { child->request_abandon(); } - - if (!handle) - { + if (!handle) { return; } - - if (handle) - { + if (handle) { handle.destroy(); } } - - [[nodiscard]] bool complete_in_final_suspend() noexcept - { + [[nodiscard]] bool complete_in_final_suspend() noexcept { std::lock_guard lock(mutex_); completed_ = true; - resume_in_progress_ = false; - - if (destroy_on_completion_) + const bool needs_deferred_finish = cancel_requested_ || destroy_on_completion_ || resume_in_progress_; + resume_in_progress_ = needs_deferred_finish; + return needs_deferred_finish; + } + void finish_resume() noexcept { + std::coroutine_handle<> handle{}; { - handle_ = {}; - return true; + std::lock_guard lock(mutex_); + resume_in_progress_ = false; + if (completed_ && destroy_on_completion_ && handle_) { + handle = handle_; + handle_ = {}; + } } - return false; + if (handle) { + handle.destroy(); + } } private: @@ -531,315 +396,276 @@ namespace ucoro bool resume_in_progress_{false}; }; - template - struct final_awaitable - { - awaitable_promise* parent; - - constexpr void await_resume() noexcept - { - } - - constexpr bool await_ready() noexcept - { - return false; - } - - std::coroutine_handle<> await_suspend(std::coroutine_handle> h) noexcept - { - auto continuation = h.promise().parent_; - auto control = h.promise().control_; - const bool destroy_on_completion = control && control->complete_in_final_suspend(); - - if (destroy_on_completion) - { - h.destroy(); - return continuation ? continuation : std::noop_coroutine(); + struct final_resume_task { + struct promise_type { + final_resume_task get_return_object() noexcept { + return final_resume_task{ + std::coroutine_handle::from_promise(*this) + }; } + std::suspend_always initial_suspend() noexcept { + return {}; + } + std::suspend_never final_suspend() noexcept { + return {}; + } + void return_void() noexcept { + } + void unhandled_exception() noexcept { + std::terminate(); + } + }; - return continuation ? continuation : std::noop_coroutine(); + std::coroutine_handle handle_{}; + + std::coroutine_handle<> release() noexcept { + auto handle = handle_; + handle_ = {}; + return handle; } }; + inline final_resume_task resume_final_continuation( + std::shared_ptr control, + std::coroutine_handle<> continuation, + std::shared_ptr parent_control) noexcept { + if (continuation) { + if (parent_control) { + parent_control->resume_continuation(continuation); + } + else { + continuation.resume(); + } + } + + if (control) { + control->finish_resume(); + } + + co_return; + } + template - struct awaitable_promise : public awaitable_promise_value, public debug_coro_promise - { - awaitable get_return_object(); - - auto final_suspend() noexcept - { - return final_awaitable{this}; + struct final_awaitable { + awaitable_promise* parent; + constexpr void await_resume() noexcept { } - - auto initial_suspend() - { - return std::suspend_always{}; + constexpr bool await_ready() noexcept { + return false; } + std::coroutine_handle<> await_suspend(std::coroutine_handle> h) noexcept { + auto continuation = h.promise().parent_; + auto control = h.promise().control_; + auto parent_control = h.promise().parent_control_; - void set_local(std::any local) - { - local_ = std::make_shared(std::move(local)); - } - - template - struct local_storage_awaiter - { - const awaitable_promise* this_; - - [[nodiscard]] constexpr bool await_ready() const noexcept { return true; } - - constexpr void await_suspend(std::coroutine_handle<>) const noexcept - { + bool needs_deferred_finish = false; + if (control) { + needs_deferred_finish = control->complete_in_final_suspend(); } - auto await_resume() const - { - if (!this_->local_) - { + if (needs_deferred_finish) { + return resume_final_continuation(std::move(control), continuation, std::move(parent_control)).release(); + } + + if (continuation) { + return continuation; + } + + return std::noop_coroutine(); + } + }; + template + struct awaitable_promise : public awaitable_promise_value, public debug_coro_promise { + awaitable get_return_object(); + auto final_suspend() noexcept { + return final_awaitable{this}; + } + auto initial_suspend() { + return std::suspend_always{}; + } + void set_local(std::any local) { + local_ = std::make_shared(std::move(local)); + } + template + struct local_storage_awaiter { + const awaitable_promise* this_; + [[nodiscard]] constexpr bool await_ready() const noexcept { return true; } + constexpr void await_suspend(std::coroutine_handle<>) const noexcept { + } + auto await_resume() const { + if (!this_->local_) { throw std::logic_error("ucoro local_storage is not set"); } - - if constexpr (std::is_void_v) - { + if constexpr (std::is_void_v) { return *this_->local_; } - else - { + else { return std::any_cast(*this_->local_); } } }; - template - auto await_transform(A&& awaiter) const - { - if constexpr (concepts::local_storage_type>) - { + auto await_transform(A&& awaiter) const { + if constexpr (concepts::local_storage_type>) { return local_storage_awaiter>>{this}; } - else if constexpr (concepts::has_user_defined_await_transformer) - { + else if constexpr (concepts::has_user_defined_await_transformer) { return await_transformer>::await_transform(std::move(awaiter)); } - else if constexpr (concepts::is_awaitable_v) - { + else if constexpr (concepts::is_awaitable_v) { static_assert(std::is_rvalue_reference_v, "co_await must be used on rvalue"); return std::forward(awaiter); } - else - { + else { static_assert(concepts::is_not_awaitable::value, "co_await must be called on an awaitable type"); } } - std::coroutine_handle<> parent_{}; + std::shared_ptr parent_control_{}; std::shared_ptr control_{std::make_shared()}; std::shared_ptr local_{}; }; - template - struct awaitable - { + struct awaitable { using promise_type = awaitable_promise; - explicit awaitable(std::coroutine_handle h) - : control_(h.promise().control_) - { + : control_(h.promise().control_) { } - - ~awaitable() noexcept - { + ~awaitable() noexcept { reset(); } - awaitable(awaitable&& t) noexcept - : control_(std::move(t.control_)) - { + : control_(std::move(t.control_)) { } - - awaitable& operator=(awaitable&& t) noexcept - { - if (&t != this) - { + awaitable& operator=(awaitable&& t) noexcept { + if (&t != this) { reset(); control_ = std::move(t.control_); } return *this; } - awaitable(const awaitable&) = delete; awaitable(awaitable&) = delete; awaitable& operator=(const awaitable&) = delete; awaitable& operator=(awaitable&) = delete; - - [[nodiscard]] constexpr bool await_ready() const noexcept - { + [[nodiscard]] constexpr bool await_ready() const noexcept { return false; } + T await_resume() { + if (control_ && control_->cancel_requested()) { + throw operation_cancelled{}; + } - T await_resume() - { auto handle = typed_handle(); return handle.promise().get_value(); } - template - auto await_suspend(std::coroutine_handle continuation) - { + auto await_suspend(std::coroutine_handle continuation) { auto handle = typed_handle(); - - if constexpr (concepts::awaitable_promise_type) - { + if constexpr (concepts::awaitable_promise_type) { handle.promise().local_ = handle.promise().local_ ? handle.promise().local_ : continuation.promise().local_; + handle.promise().parent_control_ = continuation.promise().control_; continuation.promise().control_->set_child(control_); } - handle.promise().parent_ = continuation; - if (control_) - { + if (control_) { control_->mark_started(); } return handle; } - - [[nodiscard]] bool valid() const noexcept - { + [[nodiscard]] bool valid() const noexcept { return control_ && control_->valid(); } - - void request_abandon() noexcept - { - if (control_) - { + void request_abandon() noexcept { + if (control_) { control_->request_abandon(); } } - - void cancel() noexcept - { + void cancel() noexcept { request_abandon(); } - - void reset() noexcept - { - if (control_) - { + void reset() noexcept { + if (control_) { control_->reset_or_cancel(); control_.reset(); } } - - void start() noexcept - { - if (control_) - { + void start() noexcept { + if (control_) { control_->start(); } } - - void start_detached() noexcept - { - if (control_) - { + void start_detached() noexcept { + if (control_) { control_->start_detached(); control_.reset(); } } - - void set_local(std::any local) - { + void set_local(std::any local) { auto handle = typed_handle(); assert("local has value" && !handle.promise().local_); handle.promise().set_local(std::move(local)); } - - auto detach(std::any local = {}) - { - auto launched_coro = [](awaitable lazy) mutable -> awaitable - { + auto detach(std::any local = {}) { + auto launched_coro = [](awaitable lazy) mutable -> awaitable { co_return co_await std::move(lazy); }(std::move(*this)); - - if (local.has_value()) - { + if (local.has_value()) { launched_coro.set_local(local); } - return launched_coro; } - template >>> - auto detach_with_callback(Function completion_handler) - { + auto detach_with_callback(Function completion_handler) { return detach_with_callback(std::any{}, std::move(completion_handler)); } - template >>> - auto detach_with_callback(std::any local, Function completion_handler) - { - auto launched_coro = [](awaitable lazy, auto completion_handler) mutable -> awaitable - { + auto detach_with_callback(std::any local, Function completion_handler) { + auto launched_coro = [](awaitable lazy, auto completion_handler) mutable -> awaitable { using result_wrapper = ucoro::traits::exception_with_result_t; result_wrapper result{}; - - try - { - if constexpr (std::is_void_v) - { + try { + if constexpr (std::is_void_v) { co_await std::move(lazy); result = nullptr; } - else - { + else { result = result_wrapper{co_await std::move(lazy)}; } } - catch (...) - { + catch (...) { result = result_wrapper{std::current_exception()}; } - completion_handler(std::move(result)); }(std::move(*this), std::move(completion_handler)); - - if (local.has_value()) - { + if (local.has_value()) { launched_coro.set_local(local); } - return launched_coro; } - std::shared_ptr control_; - private: - [[nodiscard]] std::coroutine_handle typed_handle() const noexcept - { + [[nodiscard]] std::coroutine_handle typed_handle() const noexcept { assert(control_ && "awaitable has no coroutine control block"); auto handle = control_->handle(); assert(handle && "awaitable has no coroutine handle"); return std::coroutine_handle::from_address(handle.address()); } }; - template - awaitable awaitable_promise::get_return_object() - { + awaitable awaitable_promise::get_return_object() { auto handle = std::coroutine_handle>::from_promise(*this); control_->attach(handle); return awaitable{handle}; } } // namespace ucoro - -namespace ucoro -{ +namespace ucoro { template - struct Callback_Awaiter_State - { + struct Callback_Awaiter_State { std::mutex mutex_; std::weak_ptr owner_; std::coroutine_handle<> fallback_handle_{}; @@ -848,10 +674,8 @@ namespace ucoro bool cancelled_{false}; std::optional result_; }; - template <> - struct Callback_Awaiter_State - { + struct Callback_Awaiter_State { std::mutex mutex_; std::weak_ptr owner_; std::coroutine_handle<> fallback_handle_{}; @@ -859,259 +683,207 @@ namespace ucoro bool completed_{false}; bool cancelled_{false}; }; - template - struct Callback_Awaiter - { + struct Callback_Awaiter { Callback_Awaiter(const Callback_Awaiter&) = delete; Callback_Awaiter& operator=(const Callback_Awaiter&) = delete; - public: explicit Callback_Awaiter(CallbackFunction&& callback_function) - : callback_function_(std::forward(callback_function)) - { + : callback_function_(std::forward(callback_function)) { } - Callback_Awaiter(Callback_Awaiter&&) noexcept = default; Callback_Awaiter& operator=(Callback_Awaiter&&) noexcept = default; - - ~Callback_Awaiter() - { + ~Callback_Awaiter() { cancel_state(); } - - constexpr bool await_ready() noexcept - { + constexpr bool await_ready() noexcept { return false; } - template - bool await_suspend(std::coroutine_handle handle) - { + bool await_suspend(std::coroutine_handle handle) { state_ = std::make_shared(); auto state = state_; - - if constexpr (concepts::awaitable_promise_type) - { + if constexpr (concepts::awaitable_promise_type) { state->owner_ = handle.promise().control_; } - else - { + else { state->fallback_handle_ = handle; } - - if (auto owner = state->owner_.lock()) - { - if (owner->cancel_requested()) - { + if (auto owner = state->owner_.lock()) { + if (owner->cancel_requested()) { cancel_state(state); return false; } } - - try - { - if constexpr (std::is_void_v) - { - callback_function_([state]() mutable - { + try { + if constexpr (std::is_void_v) { + callback_function_([state]() mutable { complete_state(state); }); } - else - { - callback_function_([state](T t) mutable - { + else { + callback_function_([state](T t) mutable { complete_state(state, std::move(t)); }); } } - catch (...) - { + catch (...) { cancel_state(state); throw; } - { std::lock_guard lock(state->mutex_); - - if (state->completed_) - { + if (state->completed_) { // The callback completed before await_suspend returned. The coroutine // must not suspend; await_resume() will consume the stored result. return false; } - - if (state->cancelled_) - { + if (state->cancelled_) { return false; } - state->await_suspend_finished_ = true; } - return true; } - - T await_resume() - { + T await_resume() { assert(state_ && "callback awaiter has no state"); - if (auto owner = state_->owner_.lock()) { - if (owner->cancel_requested()) - { + std::lock_guard lock(state_->mutex_); + if (state_->cancelled_) { throw operation_cancelled{}; } } - if constexpr (std::is_void_v) - { + if (auto owner = state_->owner_.lock()) { + if (owner->cancel_requested()) { + throw operation_cancelled{}; + } + } + if constexpr (std::is_void_v) { return; } - else - { + else { assert(state_->result_.has_value() && "callback result was not set before await_resume"); return std::move(*state_->result_); } } - private: using State = Callback_Awaiter_State; - - static void cancel_state(const std::shared_ptr& state) noexcept - { - if (!state) - { + static void cancel_state(const std::shared_ptr& state) noexcept { + if (!state) { return; } - std::lock_guard lock(state->mutex_); state->cancelled_ = true; } - - void cancel_state() noexcept - { + void cancel_state() noexcept { cancel_state(state_); } - - static void resume_state(const std::shared_ptr& state) - { + static void resume_state(const std::shared_ptr& state) { std::shared_ptr owner; std::coroutine_handle<> fallback{}; bool should_resume = false; - { std::lock_guard lock(state->mutex_); should_resume = state->await_suspend_finished_; owner = state->owner_.lock(); fallback = state->fallback_handle_; } - - if (!should_resume) - { + if (!should_resume) { return; } - - if (owner) - { + if (owner) { owner->resume_from_callback(); } - else if (fallback) - { + else if (fallback) { fallback.resume(); } } - template - static void complete_state(const std::shared_ptr& state, V&& value) - { + static void complete_state(const std::shared_ptr& state, V&& value) { + const bool owner_cancelled = [&]() noexcept { + if (auto owner = state->owner_.lock()) { + return owner->cancel_requested(); + } + return false; + }(); + { std::lock_guard lock(state->mutex_); - - if (state->completed_ || state->cancelled_) - { + if (state->completed_ || state->cancelled_) { return; } - state->completed_ = true; - state->result_.emplace(std::forward(value)); + if (owner_cancelled) { + state->cancelled_ = true; + } + else { + state->result_.emplace(std::forward(value)); + } } - resume_state(state); } + static void complete_state(const std::shared_ptr& state) { + const bool owner_cancelled = [&]() noexcept { + if (auto owner = state->owner_.lock()) { + return owner->cancel_requested(); + } + return false; + }(); - static void complete_state(const std::shared_ptr& state) - { { std::lock_guard lock(state->mutex_); - - if (state->completed_ || state->cancelled_) - { + if (state->completed_ || state->cancelled_) { return; } - state->completed_ = true; + if (owner_cancelled) { + state->cancelled_ = true; + } } - resume_state(state); } - CallbackFunction callback_function_; std::shared_ptr state_; }; - template - [[nodiscard]] auto callback_awaitable(callback&& cb) -> awaitable - { + [[nodiscard]] auto callback_awaitable(callback&& cb) -> awaitable { co_return co_await Callback_Awaiter{std::forward(cb)}; } - template - [[nodiscard]] auto coro_start(Awaitable&& coro, Local&& local, CompleteFunction completer) - { + [[nodiscard]] auto coro_start(Awaitable&& coro, Local&& local, CompleteFunction completer) { auto launched_coro = coro.detach_with_callback(std::forward(local), std::move(completer)); launched_coro.start(); return launched_coro; } - template - [[nodiscard]] auto coro_start(Awaitable&& coro, Local&& local) - { + [[nodiscard]] auto coro_start(Awaitable&& coro, Local&& local) { auto launched_coro = coro.detach(std::forward(local)); launched_coro.start(); return launched_coro; } - template - [[nodiscard]] auto coro_start(Awaitable&& coro) - { + [[nodiscard]] auto coro_start(Awaitable&& coro) { auto launched_coro = coro.detach(); launched_coro.start(); return launched_coro; } - template - void start_detached(Awaitable&& coro, Local&& local, CompleteFunction completer) - { + void start_detached(Awaitable&& coro, Local&& local, CompleteFunction completer) { auto launched_coro = coro.detach_with_callback(std::forward(local), std::move(completer)); launched_coro.start_detached(); } - template - void start_detached(Awaitable&& coro, Local&& local) - { + void start_detached(Awaitable&& coro, Local&& local) { auto launched_coro = coro.detach(std::forward(local)); launched_coro.start_detached(); } - template - void start_detached(Awaitable&& coro) - { + void start_detached(Awaitable&& coro) { auto launched_coro = coro.detach(); launched_coro.start_detached(); } - // Synchronously waits until the ucoro task completes and returns its result. // // Usage: @@ -1123,15 +895,12 @@ namespace ucoro // finish synchronously. It does not run an event loop. Do not use it to wait for work // that requires the current thread to pump asio, drogon, Qt, libuv, or another loop. template - auto sync_await(awaitable lazy, std::any local_ = {}) -> T - { + auto sync_await(awaitable lazy, std::any local_ = {}) -> T { std::mutex mtx; std::condition_variable cv; bool done = false; traits::exception_with_result_t result; - - auto launched_coro = lazy.detach_with_callback(local_, [&](traits::exception_with_result_t result_) mutable - { + auto launched_coro = lazy.detach_with_callback(local_, [&](traits::exception_with_result_t result_) mutable { { std::lock_guard lock(mtx); result = std::move(result_); @@ -1140,24 +909,18 @@ namespace ucoro cv.notify_one(); }); launched_coro.start(); - { std::unique_lock lock(mtx); cv.wait(lock, [&] { return done; }); } - - if constexpr (std::is_void_v) - { - if (result) - { + if constexpr (std::is_void_v) { + if (result) { std::rethrow_exception(result); } return; } - else - { - if (std::holds_alternative(result)) - { + else { + if (std::holds_alternative(result)) { std::rethrow_exception(std::get(result)); } return std::move(std::get(result)); diff --git a/3rd/ucoro/include/ucoro/single_thread.h b/3rd/ucoro/include/ucoro/single_thread.h new file mode 100644 index 0000000..837b7f7 --- /dev/null +++ b/3rd/ucoro/include/ucoro/single_thread.h @@ -0,0 +1,106 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "awaitable.hpp" + +namespace ucoro { + +class Single_Thread_Scheduler { +private: + using Abandoned_Task = ucoro::awaitable; + +public: + Single_Thread_Scheduler(); + ~Single_Thread_Scheduler(); + + Single_Thread_Scheduler(const Single_Thread_Scheduler&) = delete; + Single_Thread_Scheduler& operator=(const Single_Thread_Scheduler&) = delete; + + Single_Thread_Scheduler(Single_Thread_Scheduler&&) = delete; + Single_Thread_Scheduler& operator=(Single_Thread_Scheduler&&) = delete; + + void reset(); + void post(std::function fn); + void async_wait(std::function fn); + void wake(); + void stop(); + + std::size_t drain(std::size_t max_count = 1024); + + void wait_for_work(); + + template + void wait_for_work(StopPredicate should_stop) { + std::unique_lock lk(mtx_); + + cv_.wait(lk, [&] { + return stopping_ + || wake_requested_ + || exception_ + || !callbacks_.empty() + || should_stop(); + }); + + wake_requested_ = false; + } + + void wait_for_callback_for(std::chrono::milliseconds timeout); + + void set_exception(std::exception_ptr exception); + void rethrow_if_exception(); + + void cleanup_abandoned_tasks(); + + template + void abandon_remaining_tasks(TaskMap& tasks) { + std::vector garbage; + + { + std::lock_guard g(mtx_); + + cleanup_abandoned_tasks_locked(garbage); + + for (auto it = tasks.begin(); it != tasks.end();) { + if (it->second.valid()) { + it->second.request_abandon(); + abandoned_tasks_.emplace_back(std::move(it->second)); + } + + it = tasks.erase(it); + } + + wake_requested_ = true; + release_waiters_locked(); + } + + cv_.notify_one(); + } + + std::size_t abandoned_task_count(); + +private: + static bool is_ucoro_operation_cancelled(std::exception_ptr exception) noexcept; + + void release_waiters_locked(); + void cleanup_abandoned_tasks_locked(std::vector& garbage); + +private: + std::mutex mtx_; + std::condition_variable cv_; + std::queue> callbacks_; + std::queue> waiters_; + std::vector abandoned_tasks_; + std::exception_ptr exception_; + bool wake_requested_ = false; + bool stopping_ = false; +}; + +} // namespace ucoro diff --git a/3rd/ucoro/main.cmake b/3rd/ucoro/main.cmake new file mode 100644 index 0000000..e69de29 diff --git a/3rd/ucoro/src/single_thread.cpp b/3rd/ucoro/src/single_thread.cpp new file mode 100644 index 0000000..6fd7a96 --- /dev/null +++ b/3rd/ucoro/src/single_thread.cpp @@ -0,0 +1,235 @@ +#include "ucoro/single_thread.h" + +#include + +namespace ucoro { + +Single_Thread_Scheduler::Single_Thread_Scheduler() = default; +Single_Thread_Scheduler::~Single_Thread_Scheduler() = default; + +void Single_Thread_Scheduler::reset() { + std::vector garbage; + + { + std::lock_guard g(mtx_); + + stopping_ = false; + wake_requested_ = false; + exception_ = nullptr; + + cleanup_abandoned_tasks_locked(garbage); + + if (abandoned_tasks_.empty()) { + while (!callbacks_.empty()) { + callbacks_.pop(); + } + + while (!waiters_.empty()) { + waiters_.pop(); + } + } else { + // Abandoned tasks from a previous run may still be suspended on + // async_wait(). Do not discard their waiters; release them so they + // can observe cancellation and unwind. + release_waiters_locked(); + } + } + + // garbage is destroyed outside mtx_, so coroutine frames are never destroyed + // while the scheduler lock is held. +} + +void Single_Thread_Scheduler::post(std::function fn) { + { + std::lock_guard g(mtx_); + callbacks_.push(std::move(fn)); + release_waiters_locked(); + } + + cv_.notify_one(); +} + +void Single_Thread_Scheduler::async_wait(std::function fn) { + bool notify = false; + + { + std::lock_guard g(mtx_); + + if (stopping_ || exception_ || wake_requested_ || !callbacks_.empty()) { + callbacks_.push(std::move(fn)); + notify = true; + } else { + waiters_.push(std::move(fn)); + } + } + + if (notify) { + cv_.notify_one(); + } +} + +void Single_Thread_Scheduler::wake() { + { + std::lock_guard g(mtx_); + wake_requested_ = true; + release_waiters_locked(); + } + + cv_.notify_one(); +} + +void Single_Thread_Scheduler::stop() { + { + std::lock_guard g(mtx_); + stopping_ = true; + wake_requested_ = true; + release_waiters_locked(); + } + + cv_.notify_all(); +} + +std::size_t Single_Thread_Scheduler::drain(std::size_t max_count) { + std::size_t count = 0; + + while (count < max_count) { + std::function fn; + + { + std::lock_guard g(mtx_); + + if (callbacks_.empty()) { + break; + } + + fn = std::move(callbacks_.front()); + callbacks_.pop(); + } + + fn(); + ++count; + } + + return count; +} + +void Single_Thread_Scheduler::wait_for_work() { + std::unique_lock lk(mtx_); + + cv_.wait(lk, [&] { + return stopping_ + || wake_requested_ + || exception_ + || !callbacks_.empty(); + }); + + wake_requested_ = false; +} + +void Single_Thread_Scheduler::wait_for_callback_for(std::chrono::milliseconds timeout) { + std::unique_lock lk(mtx_); + + cv_.wait_for(lk, timeout, [&] { + return stopping_ + || wake_requested_ + || exception_ + || !callbacks_.empty(); + }); + + wake_requested_ = false; +} + +void Single_Thread_Scheduler::set_exception(std::exception_ptr exception) { + if (!exception) { + return; + } + + if (is_ucoro_operation_cancelled(exception)) { + return; + } + + { + std::lock_guard g(mtx_); + + if (!exception_) { + exception_ = exception; + } + + wake_requested_ = true; + release_waiters_locked(); + } + + cv_.notify_one(); +} + +void Single_Thread_Scheduler::rethrow_if_exception() { + std::exception_ptr exception; + + { + std::lock_guard g(mtx_); + exception = exception_; + exception_ = nullptr; + } + + if (exception) { + std::rethrow_exception(exception); + } +} + +void Single_Thread_Scheduler::cleanup_abandoned_tasks() { + std::vector garbage; + + { + std::lock_guard g(mtx_); + cleanup_abandoned_tasks_locked(garbage); + } + + // garbage is destroyed outside mtx_. +} + +std::size_t Single_Thread_Scheduler::abandoned_task_count() { + std::vector garbage; + std::size_t count = 0; + + { + std::lock_guard g(mtx_); + cleanup_abandoned_tasks_locked(garbage); + count = abandoned_tasks_.size(); + } + + return count; +} + +bool Single_Thread_Scheduler::is_ucoro_operation_cancelled(std::exception_ptr exception) noexcept { + if (!exception) { + return false; + } + + try { + std::rethrow_exception(exception); + } catch (const ucoro::operation_cancelled&) { + return true; + } catch (...) { + return false; + } +} + +void Single_Thread_Scheduler::release_waiters_locked() { + while (!waiters_.empty()) { + callbacks_.push(std::move(waiters_.front())); + waiters_.pop(); + } +} + +void Single_Thread_Scheduler::cleanup_abandoned_tasks_locked(std::vector& garbage) { + for (auto it = abandoned_tasks_.begin(); it != abandoned_tasks_.end();) { + if (!it->valid()) { + garbage.emplace_back(std::move(*it)); + it = abandoned_tasks_.erase(it); + } else { + ++it; + } + } +} + +} // namespace ucoro diff --git a/3rd/ucoro/tests/single_thread_scheduler_tests.cpp b/3rd/ucoro/tests/single_thread_scheduler_tests.cpp new file mode 100644 index 0000000..a9d61c2 --- /dev/null +++ b/3rd/ucoro/tests/single_thread_scheduler_tests.cpp @@ -0,0 +1,656 @@ +#include "ucoro/single_thread.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + using Scheduler = ucoro::Single_Thread_Scheduler; + + std::exception_ptr make_operation_cancelled_exception() + { + try + { + throw ucoro::operation_cancelled{}; + } + catch (...) + { + return std::current_exception(); + } + } + + std::exception_ptr make_runtime_exception() + { + try + { + throw std::runtime_error("scheduler-error"); + } + catch (...) + { + return std::current_exception(); + } + } + + ucoro::awaitable scheduler_wait_task( + Scheduler& scheduler, + std::atomic& stage + ) + { + stage.store(1, std::memory_order_release); + + co_await ucoro::callback_awaitable([&scheduler](auto done) mutable + { + scheduler.async_wait([done = std::move(done)]() mutable + { + done(); + }); + }); + + stage.store(2, std::memory_order_release); + co_return; + } + + ucoro::awaitable scheduler_wait_then_return_task( + Scheduler& scheduler + ) + { + std::atomic ignored{0}; + co_await scheduler_wait_task(scheduler, ignored); + co_return; + } + + struct Destructor_Posts_To_Scheduler + { + Scheduler* scheduler = nullptr; + std::atomic* posted_callbacks = nullptr; + + Destructor_Posts_To_Scheduler( + Scheduler& scheduler_, + std::atomic& posted_callbacks_ + ) + : scheduler(&scheduler_), posted_callbacks(&posted_callbacks_) + { + } + + Destructor_Posts_To_Scheduler(const Destructor_Posts_To_Scheduler&) = delete; + Destructor_Posts_To_Scheduler& operator=(const Destructor_Posts_To_Scheduler&) = delete; + + ~Destructor_Posts_To_Scheduler() + { + if (scheduler && posted_callbacks) + { + auto* count = posted_callbacks; + scheduler->post([count] + { + count->fetch_add(1, std::memory_order_acq_rel); + }); + } + } + }; + + ucoro::awaitable task_with_destructor_that_posts( + Scheduler& scheduler, + std::atomic& stage, + std::atomic& destructor_posted_callbacks + ) + { + Destructor_Posts_To_Scheduler guard{scheduler, destructor_posted_callbacks}; + co_await scheduler_wait_task(scheduler, stage); + co_return; + } + + void expect_operation_cancelled(std::exception_ptr exception) + { + ASSERT_TRUE(exception != nullptr); + EXPECT_THROW(std::rethrow_exception(exception), ucoro::operation_cancelled); + } +} + +TEST(SingleThreadSchedulerTest, CompileTimeProperties) +{ + static_assert(!std::is_copy_constructible_v); + static_assert(!std::is_copy_assignable_v); + static_assert(!std::is_move_constructible_v); + static_assert(!std::is_move_assignable_v); + + SUCCEED(); +} + +TEST(SingleThreadSchedulerTest, PostAndDrainRunCallbacksInFifoOrderAndRespectLimit) +{ + Scheduler scheduler; + std::vector order; + + scheduler.post([&] { order.push_back(1); }); + scheduler.post([&] { order.push_back(2); }); + scheduler.post([&] { order.push_back(3); }); + + EXPECT_EQ(scheduler.drain(2), 2u); + ASSERT_EQ(order.size(), 2u); + EXPECT_EQ(order[0], 1); + EXPECT_EQ(order[1], 2); + + EXPECT_EQ(scheduler.drain(), 1u); + ASSERT_EQ(order.size(), 3u); + EXPECT_EQ(order[2], 3); + + EXPECT_EQ(scheduler.drain(), 0u); +} + +TEST(SingleThreadSchedulerTest, ConcurrentPostFromManyThreadsDoesNotDropCallbacks) +{ + Scheduler scheduler; + std::atomic calls{0}; + + constexpr int thread_count = 4; + constexpr int callbacks_per_thread = 250; + constexpr int expected_callbacks = thread_count * callbacks_per_thread; + + std::vector posters; + posters.reserve(thread_count); + + for (int i = 0; i < thread_count; ++i) + { + posters.emplace_back([&] + { + for (int j = 0; j < callbacks_per_thread; ++j) + { + scheduler.post([&] + { + calls.fetch_add(1, std::memory_order_acq_rel); + }); + } + }); + } + + for (auto& poster : posters) + { + poster.join(); + } + + std::size_t drained = 0; + while (drained < static_cast(expected_callbacks)) + { + auto count = scheduler.drain(37); + if (count == 0) + { + break; + } + + drained += count; + } + + EXPECT_EQ(drained, static_cast(expected_callbacks)); + EXPECT_EQ(calls.load(std::memory_order_acquire), expected_callbacks); + EXPECT_EQ(scheduler.drain(), 0u); +} + +TEST(SingleThreadSchedulerTest, ReentrantPostIsQueuedAndRespectsDrainLimit) +{ + Scheduler scheduler; + std::vector order; + + scheduler.post([&] + { + order.push_back(1); + scheduler.post([&] + { + order.push_back(2); + }); + }); + + EXPECT_EQ(scheduler.drain(1), 1u); + ASSERT_EQ(order.size(), 1u); + EXPECT_EQ(order[0], 1); + + EXPECT_EQ(scheduler.drain(), 1u); + ASSERT_EQ(order.size(), 2u); + EXPECT_EQ(order[1], 2); + + scheduler.post([&] + { + order.push_back(3); + scheduler.post([&] + { + order.push_back(4); + }); + }); + + EXPECT_EQ(scheduler.drain(), 2u); + ASSERT_EQ(order.size(), 4u); + EXPECT_EQ(order[2], 3); + EXPECT_EQ(order[3], 4); +} + +TEST(SingleThreadSchedulerTest, AsyncWaitStaysPendingUntilWake) +{ + Scheduler scheduler; + std::atomic calls{0}; + + scheduler.async_wait([&] + { + calls.fetch_add(1, std::memory_order_acq_rel); + }); + + EXPECT_EQ(scheduler.drain(), 0u); + EXPECT_EQ(calls.load(std::memory_order_acquire), 0); + + scheduler.wake(); + scheduler.wait_for_work(); + + EXPECT_EQ(scheduler.drain(), 1u); + EXPECT_EQ(calls.load(std::memory_order_acquire), 1); +} + +TEST(SingleThreadSchedulerTest, WakeReleasesEachPendingWaiterOnlyOnce) +{ + Scheduler scheduler; + std::atomic calls{0}; + + scheduler.async_wait([&] + { + calls.fetch_add(1, std::memory_order_acq_rel); + }); + + EXPECT_EQ(scheduler.drain(), 0u); + EXPECT_EQ(calls.load(std::memory_order_acquire), 0); + + scheduler.wake(); + scheduler.wake(); + + EXPECT_EQ(scheduler.drain(), 1u); + EXPECT_EQ(calls.load(std::memory_order_acquire), 1); + + EXPECT_EQ(scheduler.drain(), 0u); + EXPECT_EQ(calls.load(std::memory_order_acquire), 1); +} + +TEST(SingleThreadSchedulerTest, PostReleasesWaitersAfterPostedCallback) +{ + Scheduler scheduler; + std::vector order; + + scheduler.async_wait([&] + { + order.emplace_back("waiter"); + }); + + scheduler.post([&] + { + order.emplace_back("posted"); + }); + + EXPECT_EQ(scheduler.drain(), 2u); + + ASSERT_EQ(order.size(), 2u); + EXPECT_EQ(order[0], "posted"); + EXPECT_EQ(order[1], "waiter"); +} + +TEST(SingleThreadSchedulerTest, ResetClearsCallbacksAndWaitersWhenNoAbandonedTasks) +{ + Scheduler scheduler; + std::atomic calls{0}; + + scheduler.post([&] + { + calls.fetch_add(1, std::memory_order_acq_rel); + }); + + scheduler.async_wait([&] + { + calls.fetch_add(1, std::memory_order_acq_rel); + }); + + scheduler.reset(); + + EXPECT_EQ(scheduler.drain(), 0u); + EXPECT_EQ(calls.load(std::memory_order_acquire), 0); + EXPECT_EQ(scheduler.abandoned_task_count(), 0u); +} + +TEST(SingleThreadSchedulerTest, WaitForWorkReturnsWhenCallbackIsPosted) +{ + Scheduler scheduler; + std::atomic waiter_returned{false}; + std::atomic callback_calls{0}; + + std::thread waiter([&] + { + scheduler.wait_for_work(); + waiter_returned.store(true, std::memory_order_release); + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + scheduler.post([&] + { + callback_calls.fetch_add(1, std::memory_order_acq_rel); + }); + + waiter.join(); + + EXPECT_TRUE(waiter_returned.load(std::memory_order_acquire)); + EXPECT_EQ(callback_calls.load(std::memory_order_acquire), 0); + + EXPECT_EQ(scheduler.drain(), 1u); + EXPECT_EQ(callback_calls.load(std::memory_order_acquire), 1); +} + +TEST(SingleThreadSchedulerTest, WaitForWorkPredicateCanReturnImmediately) +{ + Scheduler scheduler; + std::atomic should_stop{true}; + + scheduler.wait_for_work([&] + { + return should_stop.load(std::memory_order_acquire); + }); + + SUCCEED(); +} + +TEST(SingleThreadSchedulerTest, WaitForWorkPredicateCanBeReleasedByWake) +{ + Scheduler scheduler; + std::atomic should_stop{false}; + std::atomic waiter_returned{false}; + + std::thread waiter([&] + { + scheduler.wait_for_work([&] + { + return should_stop.load(std::memory_order_acquire); + }); + + waiter_returned.store(true, std::memory_order_release); + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + should_stop.store(true, std::memory_order_release); + scheduler.wake(); + + waiter.join(); + + EXPECT_TRUE(waiter_returned.load(std::memory_order_acquire)); +} + +TEST(SingleThreadSchedulerTest, StopReleasesWaitersAndWaitForWork) +{ + Scheduler scheduler; + std::atomic waiter_calls{0}; + std::atomic wait_for_work_returned{false}; + + scheduler.async_wait([&] + { + waiter_calls.fetch_add(1, std::memory_order_acq_rel); + }); + + std::thread waiter([&] + { + scheduler.wait_for_work(); + wait_for_work_returned.store(true, std::memory_order_release); + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + scheduler.stop(); + + waiter.join(); + + EXPECT_TRUE(wait_for_work_returned.load(std::memory_order_acquire)); + EXPECT_EQ(scheduler.drain(), 1u); + EXPECT_EQ(waiter_calls.load(std::memory_order_acquire), 1); +} + +TEST(SingleThreadSchedulerTest, WaitForCallbackForReturnsWhenCallbackExists) +{ + Scheduler scheduler; + std::atomic callback_calls{0}; + std::atomic waiter_returned{false}; + + std::thread waiter([&] + { + scheduler.wait_for_callback_for(std::chrono::seconds(1)); + waiter_returned.store(true, std::memory_order_release); + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + scheduler.post([&] + { + callback_calls.fetch_add(1, std::memory_order_acq_rel); + }); + + waiter.join(); + + EXPECT_TRUE(waiter_returned.load(std::memory_order_acquire)); + EXPECT_EQ(callback_calls.load(std::memory_order_acquire), 0); + + EXPECT_EQ(scheduler.drain(), 1u); + EXPECT_EQ(callback_calls.load(std::memory_order_acquire), 1); +} + +TEST(SingleThreadSchedulerTest, WaitForCallbackForTimeoutDoesNotReleaseAsyncWaiter) +{ + Scheduler scheduler; + std::atomic waiter_calls{0}; + std::atomic timeout_returned{false}; + + scheduler.async_wait([&] + { + waiter_calls.fetch_add(1, std::memory_order_acq_rel); + }); + + const auto start = std::chrono::steady_clock::now(); + + std::thread waiter([&] + { + scheduler.wait_for_callback_for(std::chrono::milliseconds(30)); + timeout_returned.store(true, std::memory_order_release); + }); + + waiter.join(); + + const auto elapsed = std::chrono::steady_clock::now() - start; + + EXPECT_TRUE(timeout_returned.load(std::memory_order_acquire)); + EXPECT_GE(elapsed, std::chrono::milliseconds(5)); + EXPECT_EQ(scheduler.drain(), 0u); + EXPECT_EQ(waiter_calls.load(std::memory_order_acquire), 0); + + scheduler.wake(); + + EXPECT_EQ(scheduler.drain(), 1u); + EXPECT_EQ(waiter_calls.load(std::memory_order_acquire), 1); +} + +TEST(SingleThreadSchedulerTest, SetExceptionReleasesWaitersAndRethrowsOnce) +{ + Scheduler scheduler; + std::atomic waiter_calls{0}; + + scheduler.async_wait([&] + { + waiter_calls.fetch_add(1, std::memory_order_acq_rel); + }); + + scheduler.set_exception(make_runtime_exception()); + + EXPECT_EQ(scheduler.drain(), 1u); + EXPECT_EQ(waiter_calls.load(std::memory_order_acquire), 1); + + EXPECT_THROW(scheduler.rethrow_if_exception(), std::runtime_error); + EXPECT_NO_THROW(scheduler.rethrow_if_exception()); +} + +TEST(SingleThreadSchedulerTest, OperationCancelledExceptionIsIgnored) +{ + Scheduler scheduler; + + scheduler.set_exception(make_operation_cancelled_exception()); + + EXPECT_NO_THROW(scheduler.rethrow_if_exception()); + EXPECT_EQ(scheduler.drain(), 0u); +} + +TEST(SingleThreadSchedulerTest, AbandonRemainingTasksKeepsTaskUntilReleasedByWaiter) +{ + Scheduler scheduler; + std::atomic stage{0}; + std::mutex mutex; + std::condition_variable cv; + bool completed = false; + std::exception_ptr exception; + + std::map> tasks; + + auto task = scheduler_wait_task(scheduler, stage).detach_with_callback( + [&](std::exception_ptr result) + { + { + std::lock_guard lock(mutex); + exception = result; + completed = true; + } + cv.notify_one(); + }); + + task.start(); + ASSERT_EQ(stage.load(std::memory_order_acquire), 1); + ASSERT_TRUE(task.valid()); + + tasks.emplace(1, std::move(task)); + + scheduler.abandon_remaining_tasks(tasks); + + EXPECT_TRUE(tasks.empty()); + EXPECT_EQ(scheduler.abandoned_task_count(), 1u); + EXPECT_EQ(stage.load(std::memory_order_acquire), 1); + + EXPECT_EQ(scheduler.drain(), 1u); + + { + std::unique_lock lock(mutex); + cv.wait(lock, [&] { return completed; }); + } + + EXPECT_EQ(stage.load(std::memory_order_acquire), 1); + expect_operation_cancelled(exception); + + scheduler.cleanup_abandoned_tasks(); + EXPECT_EQ(scheduler.abandoned_task_count(), 0u); +} + +TEST(SingleThreadSchedulerTest, ResetDoesNotDiscardAbandonedCallbacks) +{ + Scheduler scheduler; + std::atomic stage{0}; + std::mutex mutex; + std::condition_variable cv; + bool completed = false; + std::exception_ptr exception; + + std::map> tasks; + + auto task = scheduler_wait_task(scheduler, stage).detach_with_callback( + [&](std::exception_ptr result) + { + { + std::lock_guard lock(mutex); + exception = result; + completed = true; + } + cv.notify_one(); + }); + + task.start(); + ASSERT_EQ(stage.load(std::memory_order_acquire), 1); + tasks.emplace(1, std::move(task)); + + scheduler.abandon_remaining_tasks(tasks); + ASSERT_EQ(scheduler.abandoned_task_count(), 1u); + + scheduler.reset(); + + EXPECT_EQ(scheduler.drain(), 1u); + + { + std::unique_lock lock(mutex); + cv.wait(lock, [&] { return completed; }); + } + + EXPECT_EQ(stage.load(std::memory_order_acquire), 1); + expect_operation_cancelled(exception); + + scheduler.cleanup_abandoned_tasks(); + EXPECT_EQ(scheduler.abandoned_task_count(), 0u); +} + +TEST(SingleThreadSchedulerTest, CleanupAbandonedTasksDoesNotDestroyCoroutineFrameUnderSchedulerLock) +{ + Scheduler scheduler; + std::atomic stage{0}; + std::atomic destructor_posted_callbacks{0}; + + std::map> tasks; + + auto task = task_with_destructor_that_posts( + scheduler, + stage, + destructor_posted_callbacks + ); + + task.start(); + ASSERT_EQ(stage.load(std::memory_order_acquire), 1); + tasks.emplace(1, std::move(task)); + + scheduler.abandon_remaining_tasks(tasks); + ASSERT_TRUE(tasks.empty()); + ASSERT_EQ(scheduler.abandoned_task_count(), 1u); + + EXPECT_TRUE(scheduler.drain() >= 1u); + + // cleanup_abandoned_tasks() will erase the completed task from abandoned_tasks_. + // The coroutine frame destructor posts back into the scheduler. If cleanup held + // the scheduler mutex during destruction, this test would deadlock here. + scheduler.cleanup_abandoned_tasks(); + + // Depending on coroutine destruction timing, the destructor-posted callback may + // already have been drained by the previous drain(), or may still be queued now. + scheduler.drain(); + EXPECT_EQ(destructor_posted_callbacks.load(std::memory_order_acquire), 1); + EXPECT_EQ(scheduler.abandoned_task_count(), 0u); +} + +TEST(SingleThreadSchedulerTest, ResetCanBeUsedAfterStop) +{ + Scheduler scheduler; + std::atomic calls{0}; + + scheduler.stop(); + scheduler.reset(); + + scheduler.async_wait([&] + { + calls.fetch_add(1, std::memory_order_acq_rel); + }); + + EXPECT_EQ(scheduler.drain(), 0u); + + scheduler.wake(); + + EXPECT_EQ(scheduler.drain(), 1u); + EXPECT_EQ(calls.load(std::memory_order_acquire), 1); +} diff --git a/main.cmake b/main.cmake index 244ca91..7e3a614 100644 --- a/main.cmake +++ b/main.cmake @@ -177,7 +177,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux") ) endif() - +#include("${CMAKE_CURRENT_LIST_DIR}/3rd/ucoro/main.cmake") if(1) if (NOT GTest_FOUND) find_package(GTest) @@ -185,10 +185,17 @@ if(1) if (NOT GTest_FOUND) add_subdirectory(${CMAKE_SOURCE_DIR}/3rdParty/GTest) endif() + set(ucoro_dir "${CMAKE_CURRENT_LIST_DIR}/3rd/ucoro") + file(GLOB_RECURSE ucoro_srcs ${ucoro_dir}/*.h ${ucoro_dir}/*.hpp ${ucoro_dir}/*.cpp) target_compile_definitions(Core_Interface INTERFACE _USE_GTEST) target_link_libraries(Core_Static PUBLIC GTest::gtest GTest::gmock) + target_sources(Core_Static PUBLIC ${ucoro_srcs}) endif() +set(ucoro_dir "${CMAKE_CURRENT_LIST_DIR}/3rd/ucoro/src") +file(GLOB_RECURSE ucoro_srcs ${ucoro_dir}/*.h ${ucoro_dir}/*.hpp ${ucoro_dir}/*.cpp) +target_sources(Core_Static PUBLIC ${ucoro_srcs}) + target_link_libraries(Core_Static PUBLIC Core_Interface) target_link_libraries(Core_Static PUBLIC spdlog::spdlog) target_link_libraries(Core_Static PUBLIC asio_object) @@ -203,43 +210,6 @@ set_target_properties(Core_Static PROPERTIES set_property(TARGET Core_Static PROPERTY POSITION_INDEPENDENT_CODE ON) -#add_library(Core_lib STATIC ) -#target_link_libraries(Core_lib PRIVATE Core_Static) -# -#add_library(Core_dll SHARED) -#target_link_libraries(Core_dll PRIVATE Core_Static) -# add_executable(Core_exe ${CMAKE_CURRENT_LIST_DIR}/main.cpp) target_link_libraries(Core_exe PRIVATE Core_Static) - - -set(ucoro_cmake_dir "${CMAKE_CURRENT_LIST_DIR}/3rd/ucoro/include/ucoro") -if(EXISTS "${ucoro_cmake_dir}/CMakeLists.txt") - add_subdirectory("${ucoro_cmake_dir}" "${CMAKE_BINARY_DIR}/ucoro") -endif() -#include(${CMAKE_CURRENT_LIST_DIR}/learn_coro_code/main.cmake) - - - -#set(empty_file "${CMAKE_BINARY_DIR}/temp/empty.cpp") -#get_filename_component(_dir "${empty_file}" DIRECTORY) -#file(MAKE_DIRECTORY "${_dir}") -#file(TOUCH "${empty_file}") - - -#include("${CMAKE_SOURCE_DIR}/0_cmake_library/ninja_build_analyze/test_ninja_build_analyze.cmake") -#message("11111111111111") -#register_ninja_build_analyze_target( -# NINJA_ANALYZE_RUN_TARGET -# NINJA_ANALYZE_CLEAR_TARGET -# ninja_build_analyze_ctx -# "${CMAKE_SOURCE_DIR}" -# "${CMAKE_BINARY_DIR}" -# "Core_exe" -# "26" -# "${CMAKE_BINARY_DIR}/build_analysis" -# "ON" -# "30" -#) -#message("2222222222222") diff --git a/main.cpp b/main.cpp index 62b2754..e56270d 100644 --- a/main.cpp +++ b/main.cpp @@ -1,4 +1,8 @@ -int main() { +#include "Core/system/export.h" +int psc_main(int argc, char* argv[]) { + return Psc::redict_main_with_gtest(argc, argv, psc_main); +} - return 0; -} \ No newline at end of file +int main(int argc, char* argv[]) { + return Psc::redict_main_with_gtest(argc, argv, psc_main); +}