#pragma once /// @defgroup Tasks Tasks /// @brief Coroutine-based task handles for controlling execution and lifetime management /// @copydoc Task /// @defgroup Time Time Streams /// @brief Time-stream functionality for time-sensitive awaiters /// @defgroup Awaiters Awaiters /// @brief Versatile task awaiters that offer utility to most projects #include #include #include #include //--- User configuration header ---// #include "TasksConfig.h" //--- Debug Macros ---// #if SQUID_ENABLE_TASK_DEBUG /// @ingroup Tasks /// @brief Macro that instruments a task with a debug name string. Usually at the top of every task coroutine as @c TASK_NAME(__FUNCTION__) #define TASK_NAME(...) co_await NAMESPACE_SQUID::SetDebugName(__VA_ARGS__); #define DEBUG_STR , std::string in_debugStr #define PASS_DEBUG_STR , in_debugStr #define MANUAL_DEBUG_STR(debugStr) , debugStr #define WaitUntilImpl(...) _WaitUntil(__VA_ARGS__, #__VA_ARGS__) #define WaitWhileImpl(...) _WaitWhile(__VA_ARGS__, #__VA_ARGS__) #ifndef IN_DOXYGEN #define WaitUntil(...) WaitUntilImpl(__VA_ARGS__) #define WaitWhile(...) WaitWhileImpl(__VA_ARGS__) #endif //IN_DOXYGEN #else #define TASK_NAME(...) #define DEBUG_STR #define PASS_DEBUG_STR #define MANUAL_DEBUG_STR(...) #ifndef IN_DOXYGEN #define WaitUntil(...) _WaitUntil(__VA_ARGS__) #define WaitWhile(...) _WaitWhile(__VA_ARGS__) #endif //IN_DOXYGEN #endif //SQUID_ENABLE_TASK_DEBUG NAMESPACE_SQUID_BEGIN /// @addtogroup Tasks /// @{ //--- Task Reference Type ---// enum class eTaskRef /// Whether a handle references a task using a strong or weak reference { Strong, ///< Handle will keep the task alive (so long as there exists a valid Resumable handle) Weak, ///< Handle will not the task alive }; //--- Task Resumable Type ---// enum class eTaskResumable /// Whether a handle can be resumed (all live tasks have exactly one resumable handle and 0+ non-resumable handles) { Yes, ///< Handle is resumable No, ///< Handle is not resumable }; //--- Task Status ---// enum class eTaskStatus /// Status of a task (whether it is currently suspended or done) { Suspended, ///< Task is currently suspended Done, ///< Task has terminated and coroutine frame has been destroyed }; //--- tTaskCancelFn ---// using tTaskCancelFn = std::function; ///< CancelIf/StopIf condition function type // Forward declarations template class Task; /// Templated handle type (defaults to ) template using TaskHandle = Task; ///< Non-resumable handle that holds a strong reference to a task using WeakTask = Task; ///< Resumable handle that holds a weak reference to a task (always void return type) using WeakTaskHandle = Task; ///< Non-resumable handle that holds a weak reference to a task (always void return type) /// @} end of addtogroup Tasks /// @addtogroup Awaiters /// @{ //--- Suspend Awaiter ---// /// Awaiter class that suspends unconditionally struct Suspend : public std::suspend_always { }; //--- Stop Context ---// /// Context for a task's stop requests (undefined behavior if used after the underlying task is destroyed) struct StopContext { bool IsStopRequested() const { return *m_isStoppedPtr; } protected: friend class TaskInternalBase; StopContext(const bool* in_isStoppedPtr) : m_isStoppedPtr(in_isStoppedPtr) { } private: const bool* m_isStoppedPtr = nullptr; }; //--- GetStopContext Awaiter ---// /// Awaiter class that immediately (without suspending) yields a stop context struct GetStopContext { }; /// @} end of addtogroup Awaiters //--- Internal Implementation Header ---// #include "Private/TaskPrivate.h" // Internal use only! Do not move or include elsewhere! /// @addtogroup Tasks /// @{ //--- Task ---// /// Task is a high-level task handle used to manage the lifetime and execution of an underlying coroutine /// @details /// /// Handle Types /// ------------ /// The Task class is actually a template class that implements 4 user-level handle types: /// /// - \ref Task /// - A resumable task handle that holds a strong reference to the underlying coroutine. /// - Should be the return type of every coroutine you write. /// - Can be used to resume tasks, kill tasks, check if they are done, and access a return value. /// - Is permitted to have void return type (e.g. Task<>), which disables return value access. /// - \ref WeakTask /// - A resumable task handle that holds a weak reference to the underlying coroutine. /// - Can only be used to resume tasks, kill tasks, and check if they are done. /// - \ref TaskHandle /// - A non-resumable task handle that holds a strong reference to the underlying coroutine. /// - Can be used to kill tasks, check if they are done, and access a return value. /// - Is permitted to have void return type (e.g. TaskHandle<>), which disables return value access. /// - \ref WeakTaskHandle /// - A non-resumable task handle that holds a weak reference to the underlying coroutine. /// - Can only be used to kill tasks and check if they are done. /// /// Handle Type | Return Type | Resumable? | Ref Strength /// ------------------- | ----------- | ---------- | ------------------ /// \ref Task | | Yes | Strong /// \ref WeakTask | void | Yes | Weak /// \ref TaskHandle | | No | Strong /// \ref WeakTaskHandle | void | No | Weak /// /// Conversion Rules /// ---------------- /// /// It is possible to convert between these 4 types, but not all conversions are permitted. The rules for conversion are: /// - A conversion may remove resumability (e.g. \ref Task -> \ref TaskHandle), but cannot restore it /// - A conversion may remove return type (e.g. \ref Task -> \ref Task<>), but cannot restore it /// - A conversion may remove reference strength (e.g. \ref Task -> \ref WeakTask), but cannot restore it /// /// In simpler terms, this means: __a handle can always convert to a handle type with fewer capabilities, but not vice-versa__. /// /// Generally-speaking, it would be unsafe to convert in such a way that would add handle properties, hence the motivation for these /// conversion rules. Care has been taken, however, to provide clear human-readable compile-time error messages if and when an /// invalid conversion is attempted in code. /// /// Resumability /// ------------ /// /// For a given coroutine instance, it is impossible to have more than a single resumable handle that references it at runtime. /// We refer to this as the "single-resumer rule". Because both Task and WeakTask are move-only types that cannot be copy-constructed /// or copy-assigned from other handles, this guarantees at compile-time that there will never be two handles that are able to resume /// the same underlying coroutine. This compile-time guarantee was implemented after many insidious bugs emerged in gameplay code /// written using early versions of the Squid::Tasks library. /// /// When a task's single-resumer handle is destroyed, the task is immediately killed. If a coroutine were able to remain suspended /// without the possibility of ever being resumed again, then any task waiting for it to terminate would deadlock. For this reason, /// Squid::Tasks enforces that all coroutines must have a valid resumable handle at all times, otherwise they are immediately killed. /// /// (If you are unfamiliar with what it meant by "move-only type", we recommend you research "C++ move semantics" to familiarize /// yourself.) /// /// Lifetime Management /// ------------------- /// /// The default lifetime of a Task's underlying coroutine is determined by the handles the refer to it: /// - The underlying coroutine will be killed immediately if it is no longer referenced by a resumable handle ( \ref Task/ \ref WeakTask). /// - The underlying coroutine will be killed immediately if it ever has zero strong references remaining to it ( \ref Task/ \ref TaskHandle). /// /// This lifetime management model is essentially the same as a strong-pointer/weak-pointer model, with the added constraint that /// tasks are killed as soon as they can no longer logically be resumed. /// /// @tparam tRet Return type of the underlying coroutine (can be void if the coroutine does not co_return a value) /// @tparam RefType Whether this handle holds a strong or weak reference to the underlying coroutine /// @tparam Resumable Whether this handle can be used to resume the underlying coroutine template class Task { public: /// @cond using tTaskInternal = TaskInternal; using promise_type = TaskPromise; /// @endcond #define NONVOID_ONLY template ::value>* = nullptr> // Prohibit illegal task types static_assert(RefType == eTaskRef::Strong || std::is_void::value, "Illegal task type (cannot combine weak reference type with non-void return type"); Task() /// Default constructor (constructs an invalid handle) { } Task(nullptr_t) /// Null-pointer constructor (constructs an invalid handle) { } Task(std::shared_ptr in_taskInternal) /// @private : m_taskInternal(in_taskInternal) { AddRef(); } Task(std::coroutine_handle in_coroHandle) /// @private : m_taskInternal(std::make_shared(in_coroHandle)) { AddRef(); } Task(const Task& in_otherTask) /// Copy constructor (TaskHandle/WeakTaskHandle only) : m_taskInternal(in_otherTask.GetInternalTask()) { static_assert(IsCopyable(), "Cannot copy-construct Task/WeakTask (only TaskHandle/WeakTaskHandle)"); AddRef(); } Task(Task&& in_otherTask) noexcept /// Move constructor : m_taskInternal(std::move(in_otherTask.m_taskInternal)) { // NOTE: No need to alter logical reference here (this is a move) } Task& operator=(nullptr_t) noexcept /// Null-pointer assignment operator (makes the handle invalid) { RemoveRef(); // Remove logical reference from old internal task m_taskInternal = nullptr; return *this; } Task& operator=(const Task& in_otherTask) /// Copy assignment operator (TaskHandle/WeakTaskHandle only) { static_assert(IsCopyable(), "Cannot copy-assign Task/WeakTask (only TaskHandle/WeakTaskHandle)"); RemoveRef(); // Remove logical reference from our current internal task m_taskInternal = in_otherTask.m_taskInternal; AddRef(); return *this; } Task& operator=(Task&& in_otherTask) noexcept /// Move assignment operator { // If the internal task that we're about to move over can never be resumed again, kill it immediately KillIfResumable(); RemoveRef(); // Remove logical reference from old internal task // NOTE: No need to add logical reference here (this is a move) m_taskInternal = std::move(in_otherTask.m_taskInternal); return *this; } ~Task() /// Destructor { RemoveRef(); // Remove logical reference task // If the internal task can never be resumed again, kill it immediately KillIfResumable(); } bool IsValid() const /// Returns whether the underlying coroutine is valid { return m_taskInternal.get(); } operator bool() const /// Conversion-to-bool that yields whether an underlying coroutine is set for the task { return IsValid(); } bool IsDone() const /// Returns whether the task has terminated { return IsValid() ? m_taskInternal->IsDone() : true; } bool IsStopRequested() const /// Returns whether a stop request has been issued for the task { return IsValid() ? m_taskInternal->IsStopRequested() : true; } void RequestStop() /// Issues a request for the task to terminate gracefully as soon as possible { if(IsValid()) { m_taskInternal->RequestStop(); // Tell sub-tasks to stop, as well } } void Kill() /// Immediately terminates the task { // NOTE: Killing a task immediately destroys the coroutine and all of the coroutine's local variables if(IsValid()) { m_taskInternal->Kill(); } } NONVOID_ONLY std::optional TakeReturnValue() /// Attempts to take the task's return value (throws error if return value is either orphaned or was already taken) { SQUID_RUNTIME_CHECK(IsValid(), "Tried to retrieve return value from an invalid handle"); return GetInternalTask()->TakeReturnValue(); } eTaskStatus Resume() /// Resumes the task (Task/WeakTask only) { static_assert(IsResumable(), "Cannot call Resume() on a TaskHandle/WeakTaskHandle"); return IsValid() ? m_taskInternal->Resume() : eTaskStatus::Done; } #if SQUID_ENABLE_TASK_DEBUG std::string GetDebugName(std::optional in_formatter = {}) const /// Gets this task's debug name (use TASK_NAME to set the debug name) { const char* defaultRetVal = Resumable == eTaskResumable::Yes ? "[empty task]" : "[empty task handle]"; auto debugName = IsValid() ? m_taskInternal->GetDebugName() : defaultRetVal; return in_formatter ? in_formatter.value().Format(debugName) : debugName; } std::string GetDebugStack(std::optional in_formatter = {}) const /// Gets this task's debug stack (use TASK_NAME to set a task's debug name) { if(IsValid()) { return in_formatter ? in_formatter.value().Format(m_taskInternal->GetDebugStack()) : m_taskInternal->GetDebugStack(); } return GetDebugName(in_formatter); } #else std::string GetDebugName(std::optional in_formatter = {}) const /// @private { return ""; // Returns an empty string when task debug is disabled } std::string GetDebugStack(std::optional in_formatter = {}) const /// @private { return ""; // Returns an empty string when task debug is disabled } #endif //SQUID_ENABLE_TASK_DEBUG #if SQUID_USE_EXCEPTIONS std::exception_ptr GetUnhandledException() const /// Gets any unhandled exceptions thrown by the task { SQUID_RUNTIME_CHECK(IsValid(), "Tried to retrieve unhandled exception from an invalid handle"); return m_taskInternal->GetUnhandledException(); } void RethrowUnhandledException() const /// Rethrows any unhandled exceptions thrown by the task { if(auto e = m_taskInternal->GetUnhandledException()) { std::rethrow_exception(e); } } #else void RethrowUnhandledException() const /// @private { } #endif //SQUID_USE_EXCEPTIONS // Task conversion methods template operator Task() const & /// @private { constexpr bool isLegalReturnTypeConversion = std::is_void::value || std::is_same::value; constexpr bool isLegalTypeConversion = IsStrong() && IsResumable(); static_assert(isLegalTypeConversion, "Cannot promote WeakTask/TaskHandle/WeakTaskHandle to Task"); static_assert(!isLegalTypeConversion || isLegalReturnTypeConversion, "Mismatched return type (invalid return type conversion)"); static_assert(!isLegalTypeConversion || !isLegalReturnTypeConversion, "Cannot copy Task -> Task because it is non-copyable (try std::move(task))"); return {}; } template operator Task() && /// @private { constexpr bool isLegalReturnTypeConversion = std::is_void::value || std::is_same::value; constexpr bool isLegalTypeConversion = IsStrong() && IsResumable(); static_assert(isLegalTypeConversion, "Cannot promote WeakTask/TaskHandle/WeakTaskHandle to Task"); static_assert(!isLegalTypeConversion || isLegalReturnTypeConversion, "Cannot convert tasks to non-void return type (invalid return type conversion)"); // Move-to-void conversion (applies to all types) return MoveToTask(); } operator WeakTask() const & /// @private Copy-convert to WeakTask (always illegal) { static_assert(IsResumable(), "Cannot convert TaskHandle -> WeakTask (invalid resumability conversion"); static_assert(!IsResumable(), "Cannot copy Task -> WeakTask because it is non-copyable (try std::move(task))"); return {}; } operator WeakTask() && /// @private Move-convert to WeakTask (sometimes legal) { static_assert(IsResumable(), "Cannot convert TaskHandle -> WeakTask (invalid resumability conversion)"); return MoveToTask(); } operator TaskHandle() const /// @private { static_assert(IsStrong(), "Cannot convert WeakTask/WeakTaskHandle -> TaskHandle (invalid reference-strength conversion)"); return CopyToTask(); } template operator TaskHandle() const /// @private { constexpr bool isLegalReturnTypeConversion = std::is_void::value || std::is_same::value; static_assert(IsStrong(), "Cannot convert WeakTask/WeakTaskHandle -> TaskHandle (invalid reference-strength conversion)"); static_assert(!IsStrong() || isLegalReturnTypeConversion, "Mismatched return type (invalid return type conversion)"); return CopyToTask(); } operator WeakTaskHandle() const /// @private { // Convert anything to a weak task handle return CopyToTask(); } // Cancel-If Methods /// Returns wrapper task that kills this task when the given function returns true. Returns whether wrapped task was canceled. /// Task return value will be bool if wrapped task had void return type, otherwise std::optional. auto CancelIf(tTaskCancelFn in_cancelFn) && { return CancelTaskIf(std::move(*this), in_cancelFn); } /// Returns wrapper task that kills this task when a stop request is issued on it. Returns whether wrapped task was canceled. /// Task return value will be bool if wrapped task had void return type, otherwise std::optional. auto CancelIfStopRequested() && /// { return std::move(*this).CancelIf([this] { return IsStopRequested(); }); } auto CancelIf(tTaskCancelFn in_cancelFn) & /// @private Illegal lvalue implementation { static_assert(static_false::value, "Cannot call CancelIf() on an lvalue (try std::move(task).CancelIf())"); return CancelTaskIf(std::move(*this), in_cancelFn); } auto CancelIfStopRequested() & /// @private Illegal lvalue implementation { static_assert(static_false::value, "Cannot call CancelIfStopRequested() on an lvalue (try std::move(task).CancelIfStopRequested())"); return std::move(*this).CancelIf([this] { return IsStopRequested(); }); } // Stop-If Methods /// @brief Returns wrapper task that requests a stop on this task when the given function returns true, then waits for the task to terminate (without timeout). /// @details Task returns whether wrapped task was canceled. Task return value will be bool if wrapped task had void return type, otherwise std::optional. auto StopIf(tTaskCancelFn in_cancelFn) && /// Returns wrapper task that requests a stop on this task when the given function returns true { return StopTaskIf(std::move(*this), in_cancelFn); } auto StopIf(tTaskCancelFn in_cancelFn) & /// @private Illegal lvalue implementation { static_assert(static_false::value, "Cannot call StopIf() on an lvalue (try std::move(task).StopIf())"); return StopTaskIf(std::move(*this), in_cancelFn); } #if SQUID_ENABLE_GLOBAL_TIME /// @brief Returns wrapper task that requests a stop on this task when the given function returns true, then waits for the task to terminate (with timeout in the global time-stream). /// @details Task returns whether wrapped task was canceled. Task return value will be bool if wrapped task had void return type, otherwise std::optional. auto StopIf(tTaskCancelFn in_cancelFn, tTaskTime in_timeout) && { // Cannot be called unless SQUID_ENABLE_GLOBAL_TIME has been set in TasksConfig.h. return StopTaskIf(std::move(*this), in_cancelFn, in_timeout); } auto StopIf(tTaskCancelFn in_cancelFn, tTaskTime in_timeout) & /// @private Illegal lvalue implementation { static_assert(static_false::value, "Cannot call StopIf() on an lvalue (try std::move(task).StopIf())"); return StopTaskIf(std::move(*this), in_cancelFn, in_timeout); } #else auto StopIf(tTaskCancelFn in_cancelFn, tTaskTime in_timeout) && /// @private Illegal global-time implementation { static_assert(static_false::value, "Global task time not enabled (see SQUID_ENABLE_GLOBAL_TIME in TasksConfig.h)"); return StopTaskIf(std::move(*this), in_cancelFn); } auto StopIf(tTaskCancelFn in_cancelFn, tTaskTime in_timeout) & /// @private Illegal lvalue implementation { static_assert(static_false::value, "Global task time not enabled (see TasksConfig.h)"); return StopTaskIf(std::move(*this), in_cancelFn); } #endif //SQUID_ENABLE_GLOBAL_TIME /// @brief Returns wrapper task that requests a stop on this task when the given function returns true, then waits for the task to terminate (with timeout in a given time-stream). /// @details Task returns whether wrapped task was canceled. Task return value will be bool if wrapped task had void return type, otherwise std::optional. template auto StopIf(tTaskCancelFn in_cancelFn, tTaskTime in_timeout, tTimeFn in_timeFn) && { return StopTaskIf(std::move(*this), in_cancelFn, in_timeout, in_timeFn); } template auto StopIf(tTaskCancelFn in_cancelFn, tTaskTime in_timeout, tTimeFn in_timeFn) & /// @private Illegal lvalue implementation { static_assert(static_false::value, "Cannot call StopIf() on an lvalue (try std::move(task).StopIf())"); return StopTaskIf(std::move(*this), in_cancelFn, in_timeout, in_timeFn); } private: /// @cond template friend struct TaskAwaiterBase; template friend class Task; friend class TaskInternalBase; /// @endcond // Task Internal Storage std::shared_ptr m_taskInternal; // Casts the internal task storage pointer to a concrete (non-TaskInternalBase) pointer std::shared_ptr GetInternalTask() const { // We can safely downcast from TaskInternalBase to TaskInternal return std::static_pointer_cast(m_taskInternal); } // Copy/Move Implementations template Task CopyToTask() const { Task ret; ret.m_taskInternal = m_taskInternal; ret.AddRef(); return ret; } template Task MoveToTask() { Task ret; ret.m_taskInternal = m_taskInternal; ret.AddRef(); RemoveRef(); m_taskInternal = nullptr; return ret; } // Logical reference management void AddRef() { if(m_taskInternal) { if(RefType == eTaskRef::Strong) { m_taskInternal->AddLogicalRef(); } } } void RemoveRef() { if(m_taskInternal) { if(RefType == eTaskRef::Strong) { m_taskInternal->RemoveLogicalRef(); } } } void KillIfResumable() { if (IsResumable() && IsValid()) { Kill(); } } // Constexpr Helpers static constexpr bool IsResumable() { return Resumable == eTaskResumable::Yes; } static constexpr bool IsStrong() { return RefType == eTaskRef::Strong; } static constexpr bool IsCopyable() { return !IsResumable(); } #undef NONVOID_ONLY }; /// @} end of addtogroup Tasks /// @addtogroup Time /// @details /// /// Time-Streams /// ------------ /// /// Every game project has its own method of updating and measuring game time. Most games feature multiple different "time-streams", such /// as "game time", "real time", "editor time", "paused time", "audio time", etc... Because of this, the Squid::Tasks library requires each /// time-sensitive awaiter (e.g. ```WaitSeconds()```, ```Timeout()```, etc) to be presented with a time-stream function that returns the current /// time in the desired time-stream. By convention, these time-streams are passed as functions into the final argument of time-sensitive /// awaiters. /// /// Enabling Global Time Support /// ---------------------------- /// /// For less-complex projects it can be desirable to default to a "global time-stream" that removes the requirement to explicitly pass a /// time-stream function into time-sensitive awaiters. To enable this functionality, the user must set ```SQUID_ENABLE_GLOBAL_TIME``` in /// TasksConfig.h and implement a special function called Squid::GetTime(). Failure to define this function will result in a linker error. /// /// The Squid::GetTime() function should return a floating-point value representing the number of seconds since the program started running. /// Here is an example Squid::GetTime() function implementation from within the ```main.cpp``` file of a sample project: /// /// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~.cpp /// NAMESPACE_SQUID_BEGIN /// tTaskTime GetTime() /// { /// return (tTaskTime)TimeSystem::GetTime(); /// } /// NAMESPACE_SQUID_END /// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /// /// It is recommended to save off the current time value at the start of each game frame, returning that saved value from within ```Squid::GetTime()```. /// The reason for this is that, within a single frame, you likely want all of the tasks to behave as if they are updating at the same time. /// By providing the same exact time value to all Tasks that are resumed within a given update, the software is more likely to behave in a stable /// and predictable manner. /// @{ /// Helper function to elapsed time in a given time-stream template tTaskTime GetTimeSince(tTaskTime in_t, tTimeFn in_timeFn) { return in_timeFn() - in_t; } #if SQUID_ENABLE_GLOBAL_TIME /// @brief User-defined global time-stream function (must be implemented if SQUID_ENABLE_GLOBAL_TIME is set, otherwise /// there will be a linker error) tTaskTime GetGlobalTime(); /// Global time-stream function used internally by Squid::Tasks (requires SQUID_ENABLE_GLOBAL_TIME) inline auto GlobalTime() { return &GetGlobalTime; } /// Helper function to elapsed time in the global time-stream (requires SQUID_ENABLE_GLOBAL_TIME) inline tTaskTime GetTimeSince(tTaskTime in_t) { return GetTimeSince(in_t, GlobalTime()); } #else template tTaskTime GetTimeSince(tTaskTime in_t) { static_assert(static_false::value, "Global task time not enabled (see SQUID_ENABLE_GLOBAL_TIME in TasksConfig.h)"); return tTaskTime(0); } #endif //SQUID_ENABLE_GLOBAL_TIME /// @} end of addtogroup Time /// @addtogroup Awaiters /// @{ //--- All/Any/Select Tasks ---// /// @private struct TaskWrapper { public: TaskWrapper(Task<> in_task) : task(std::move(in_task)) {} ~TaskWrapper() {} Task<> task; template static std::shared_ptr Wrap(Task in_task) { return std::make_shared(std::move(in_task)); } template static std::shared_ptr Wrap(tReadyFn in_readyFn) { auto task = [](tReadyFn in_readyFn) -> Task<> { co_await in_readyFn; }(in_readyFn); return std::make_shared(std::move(task)); } }; /// @private struct TaskSingleEntry { template TaskSingleEntry(Task in_task) : taskWrapper(TaskWrapper::Wrap(std::move(in_task))) { } template TaskSingleEntry(tReadyFn in_readyFn) : taskWrapper(TaskWrapper::Wrap(in_readyFn)) { } auto Resume() { return taskWrapper->task.Resume(); } std::shared_ptr taskWrapper; }; /// @private template struct TaskSelectEntry { template TaskSelectEntry(tValue in_value, Task in_task) : value(in_value) , taskWrapper(TaskWrapper::Wrap(std::move(in_task))) { } template TaskSelectEntry(tValue in_value, tReadyFn in_readyFn) : value(in_value) , taskWrapper(TaskWrapper::Wrap(in_readyFn)) { } auto Resume() { return taskWrapper->task.Resume(); } auto GetValue() { return value; } tValue value; std::shared_ptr taskWrapper; }; /// @cond #define TASK_NAME_ENTRIES(name, entries) \ TASK_NAME(name, [entries]() { \ std::string debugStr; \ for(auto entry : entries) \ { \ debugStr += debugStr.size() ? "\n" : "\n`"; \ debugStr += entry.taskWrapper->task.GetDebugStack(); \ } \ debugStr += "`\n"; \ return debugStr; \ }); #define TASK_NAME_ENTRIES_ALL(name, entries) \ TASK_NAME(name, [entries]() { \ std::string debugStr; \ for(auto entry : entries) \ { \ debugStr += debugStr.size() ? "\n" : "\n`"; \ debugStr += entry.taskWrapper->task.GetDebugStack() + (entry.taskWrapper->task.IsDone() ? " [DONE]" : " [RUNNING]"); \ } \ debugStr += "`\n"; \ return debugStr; \ }); /// @endcond /// Awaiter task that manages a set of other awaiters and waits until at least one of them is done inline Task<> WaitForAny(std::vector in_entries) { TASK_NAME_ENTRIES(__FUNCTION__, in_entries); for(auto& entry : in_entries) { co_await AddStopTask(entry.taskWrapper->task); // Setup stop-request propagation } while(true) { for(auto& entry : in_entries) { if(entry.Resume() == eTaskStatus::Done) { co_return; } } co_await Suspend(); } } /// Awaiter task that manages a set of other awaiters and waits until all of them are done inline Task<> WaitForAll(std::vector in_entries) { TASK_NAME_ENTRIES_ALL(__FUNCTION__, in_entries); for(auto& entry : in_entries) { co_await AddStopTask(entry.taskWrapper->task); // Setup stop-request propagation } while(true) { bool allDone = true; for(auto& entry : in_entries) { if(entry.Resume() != eTaskStatus::Done) { allDone = false; } } if(allDone) { co_return; // Done! } co_await Suspend(); } } /// Awaiter task that behaves like WaitForAny(), but returns a value associated with whichever awaiter finishes first template Task Select(std::vector> in_entries) { TASK_NAME_ENTRIES(__FUNCTION__, in_entries); for(auto& entry : in_entries) { co_await AddStopTask(entry.taskWrapper->task); // Setup stop-request propagation } while(true) { for(size_t i = 0; i < in_entries.size(); ++i) { if(in_entries[i].Resume() == eTaskStatus::Done) { co_return in_entries[i].GetValue(); } } co_await Suspend(); } co_return tValue{}; } #ifdef IN_DOXYGEN /// Awaiter function that waits until a given functor returns true inline Task<> WaitUntil(tTaskReadyFn in_readyFn) {} /// Awaiter function that waits until a given functor returns false inline Task<> WaitWhile(tTaskReadyFn in_readyFn) {} #endif // IN_DOXYGEN inline Task<> _WaitUntil(tTaskReadyFn in_readyFn DEBUG_STR) /// @private { TASK_NAME("WaitUntil", [debugStr = FormatDebugString(in_debugStr)]{ return debugStr; }); co_await in_readyFn; // Wait until the ready functor returns true } inline Task<> _WaitWhile(tTaskReadyFn in_readyFn DEBUG_STR) /// @private { TASK_NAME("WaitWhile", [debugStr = FormatDebugString(in_debugStr)]{ return debugStr; }); co_await[&in_readyFn]{ return !in_readyFn(); }; // Wait until the ready function returns false } /// Awaiter function that waits forever (only for use in tasks that will be killed externally) inline Task<> WaitForever() { return _WaitUntil([]() { return false; } MANUAL_DEBUG_STR("WaitForever")); } /// Awaiter function that waits N seconds in a given time-stream template Task WaitSeconds(tTaskTime in_seconds, tTimeFn in_timeFn) { auto startTime = in_timeFn(); TASK_NAME(__FUNCTION__, [in_timeFn, startTime, in_seconds] { return std::to_string(GetTimeSince(startTime, in_timeFn)) + "/" + std::to_string(in_seconds); }); auto IsTimerUp = [in_timeFn, startTime, in_seconds] { return GetTimeSince(startTime, in_timeFn) >= in_seconds; }; co_await IsTimerUp; // Wait until the timer is up co_return in_timeFn() - startTime - in_seconds; } /// Awaiter function that wraps a given task, canceling it after N seconds in a given time-stream. Returns whether it timed-out or not. template auto Timeout(Task&& in_task, tTaskTime in_seconds, tTimeFn in_timeFn) { auto IsTimerUp = [in_timeFn, startTime = in_timeFn(), in_seconds]{ return GetTimeSince(startTime, in_timeFn) >= in_seconds; }; return CancelTaskIf(std::move(in_task), IsTimerUp); } /// Awaiter function that calls a given function after N seconds in a given time-stream template Task<> DelayCall(tTaskTime in_delaySeconds, tFn in_fn, tTimeFn in_timeFn) { TASK_NAME(__FUNCTION__); // Call function after N seconds co_await WaitSeconds(in_delaySeconds, in_timeFn); in_fn(); } #if SQUID_ENABLE_GLOBAL_TIME /// Awaiter function that waits N seconds in the global time-stream (requires SQUID_ENABLE_GLOBAL_TIME) inline Task WaitSeconds(tTaskTime in_seconds) { return WaitSeconds(in_seconds, GlobalTime()); } /// Awaiter function that wraps a given task, canceling it after N seconds in the global time-stream. Returns whether it timed-out or not. (requires SQUID_ENABLE_GLOBAL_TIME) template auto Timeout(Task&& in_task, tTaskTime in_seconds) { return Timeout(std::move(in_task), in_seconds, GlobalTime()); } /// Awaiter function that calls a given function after N seconds in the global time-stream (requires SQUID_ENABLE_GLOBAL_TIME) template Task<> DelayCall(tTaskTime in_delaySeconds, tFn in_fn) { return DelayCall(in_delaySeconds, in_fn, GlobalTime()); } #else template Task WaitSeconds(tTaskTime in_seconds) /// @private Illegal global-time implementation { static_assert(static_false::value, "Global task time not enabled (see SQUID_ENABLE_GLOBAL_TIME in TasksConfig.h)"); return Task{}; } template auto Timeout(Task&& in_task, tTaskTime in_seconds) /// @private Illegal global-time implementation { static_assert(static_false::value, "Global task time not enabled (see SQUID_ENABLE_GLOBAL_TIME in TasksConfig.h)"); return Task<>{}; } template static Task<> DelayCall(tTaskTime in_delaySeconds, tFn in_fn) /// @private Illegal global-time implementation { static_assert(static_false::value, "Global task time not enabled (see SQUID_ENABLE_GLOBAL_TIME in TasksConfig.h)"); return Task<>{}; } #endif //SQUID_ENABLE_GLOBAL_TIME //--- Cancel-If Implementation ---// template Task> CancelIfImpl(Task in_task, tTaskCancelFn in_cancelFn) /// @private { TASK_NAME("CancelIf", [taskHandle = TaskHandle(in_task)]{ return taskHandle.GetDebugStack(); }); co_await AddStopTask(in_task); // Setup stop-request propagation while(true) { if(in_cancelFn && in_cancelFn()) { co_return{}; } auto taskStatus = in_task.Resume(); if(taskStatus == eTaskStatus::Done) { co_return in_task.TakeReturnValue(); } co_await Suspend(); } co_return{}; } inline Task CancelIfImpl(Task<> in_task, tTaskCancelFn in_cancelFn) /// @private { TASK_NAME("CancelIf", [taskHandle = TaskHandle<>(in_task)]{ return taskHandle.GetDebugStack(); }); co_await AddStopTask(in_task); // Setup stop-request propagation while(true) { if(in_cancelFn && in_cancelFn()) { co_return false; } auto taskStatus = in_task.Resume(); if(taskStatus == eTaskStatus::Done) { co_return true; } co_await Suspend(); } co_return false; } template auto CancelTaskIf(Task&& in_task, tTaskCancelFn in_cancelFn) /// @private { static_assert(RefType == eTaskRef::Strong && Resumable == eTaskResumable::Yes, "Cannot call CancelIf() on WeakTask, TaskHandle or WeakTaskHandle"); return CancelIfImpl(std::move(in_task), in_cancelFn); } //--- Stop-If Implementation ---// template Task> StopIfImpl(Task in_task, tTaskCancelFn in_cancelFn, std::optional in_timeout, tTimeFn in_timeFn) /// @private { TASK_NAME("StopIf", [taskHandle = TaskHandle(in_task), in_timeout]{ return std::string("timeout = ") + (in_timeout ? std::to_string(in_timeout.value()) : "none") + ", task = " + taskHandle.GetDebugStack(); }); co_await AddStopTask(in_task); // Setup stop-request propagation while(true) { if(!in_task.IsStopRequested() && in_cancelFn && in_cancelFn()) { in_task.RequestStop(); if(in_timeout.has_value()) { co_return co_await Timeout(std::move(in_task), in_timeout.value(), in_timeFn); } } auto taskStatus = in_task.Resume(); if(taskStatus == eTaskStatus::Done) { co_return in_task.TakeReturnValue(); } co_await Suspend(); } } template Task StopIfImpl(Task<> in_task, tTaskCancelFn in_cancelFn, std::optional in_timeout, tTimeFn in_timeFn) /// @private { TASK_NAME("StopIf", [taskHandle = TaskHandle<>(in_task), in_timeout]{ return std::string("timeout = ") + (in_timeout ? std::to_string(in_timeout.value()) : "none") + ", task = " + taskHandle.GetDebugStack(); }); co_await AddStopTask(in_task); // Setup stop-request propagation while(true) { if(!in_task.IsStopRequested() && in_cancelFn && in_cancelFn()) { in_task.RequestStop(); if(in_timeout) { co_return co_await Timeout(std::move(in_task), in_timeout.value(), in_timeFn); } } auto taskStatus = in_task.Resume(); if(taskStatus == eTaskStatus::Done) { co_return true; } co_await Suspend(); } co_return false; } template auto StopTaskIf(Task&& in_task, tTaskCancelFn in_cancelFn) /// @private { return StopIfImpl(std::move(in_task), in_cancelFn, {}, (float(*)())nullptr); } #if SQUID_ENABLE_GLOBAL_TIME template auto StopTaskIf(Task&& in_task, tTaskCancelFn in_cancelFn, tTaskTime in_timeout) /// @private { return StopIfImpl(std::move(in_task), in_cancelFn, in_timeout, GlobalTime()); // Default time function to global-time } #else template auto StopTaskIf(Task&& in_task, tTaskCancelFn in_cancelFn, tTaskTime in_timeout) /// @private Illegal global-time implementation { static_assert(static_false::value, "Global task time not enabled (see SQUID_ENABLE_GLOBAL_TIME in TasksConfig.h)"); return StopIfImpl(std::move(in_task), in_cancelFn, in_timeout, nullptr); // Default time function to global-time } #endif //SQUID_ENABLE_GLOBAL_TIME template auto StopTaskIf(Task&& in_task, tTaskCancelFn in_cancelFn, tTaskTime in_timeout, tTimeFn in_timeFn) /// @private { // See forward-declaration for default arguments static_assert(RefType == eTaskRef::Strong && Resumable == eTaskResumable::Yes, "Cannot call StopIf() on WeakTask, TaskHandle or WeakTaskHandle"); return StopIfImpl(std::move(in_task), in_cancelFn, in_timeout, in_timeFn); } /// @} end of addtogroup Tasks NAMESPACE_SQUID_END