-
Notifications
You must be signed in to change notification settings - Fork 11
/
TaskManager.h
219 lines (201 loc) · 8.39 KB
/
TaskManager.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
#pragma once
/// @defgroup TaskManager Task Manager
/// @brief Manager that runs and resumes a collection of tasks.
/// @{
///
/// A TaskManager is a simple manager class that holds an ordered list of tasks and resumes them whenever it is updated.
///
/// Running Tasks
/// -------------
/// There are two primary ways to run tasks on a task manager.
///
/// The first method (running an "unmanaged task") is to pass a task into @ref TaskManager::Run(). This will move the task
/// into the task manager and return a @ref TaskHandle that can be used to observe and manage the lifetime of the task (as well
/// as potentially take a return value after the task finishes). With unmanaged tasks, the task manager only holds a weak
/// reference to the task, meaning that the @ref TaskHandle returned by @ref TaskManager::Run() is the only remaining strong
/// reference to the task. Because of this, the caller is entirely responsible for managing the lifetime of the task.
///
/// The second method (running a "managed task") is to pass a task into @ref TaskManager::RunManaged(). Like
/// @ref TaskManager::Run(), this will move the task into the task manager and return a @ref WeakTaskHandle that can be used to
/// observe the lifetime of the task (as well as manually kill it, if desired). Unlike unmanaged tasks, the task manager
/// stores a strong reference to the task. Because of this, that caller is not responsible for managing the lifetime of
/// the task. This difference in task ownership means that (unlike an unmanaged task) a managed task can be thought of as
/// a "fire-and-forget" task that will run until either it finishes or until something else explicitly kills it.
///
/// Order of Execution
/// ------------------
/// The ordering of task updates within a call to @ref TaskManager::Update() is stable, meaning that the first task that
/// is run on a task manager will remain the first to resume, no matter how many other tasks are run on the task manager
/// (or terminate) in the meantime.
///
/// Integration into Actor Classes
/// ------------------------------
/// Consider the following example of a TaskManager that has been integrated into a TaskActor base class:
///
/// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp}
///
/// class TaskActor : public Actor
/// {
/// public:
/// virtual void OnInitialize() override // Automatically called when this enemy enters the scene
/// {
/// Actor::OnInitialize(); // Call the base Actor function
/// m_taskMgr.RunManaged(ManageActor()); // Run main actor task as a fire-and-forget "managed task"
/// }
///
/// virtual void Tick(float in_dt) override // Automatically called every frame
/// {
/// Actor::Tick(in_dt); // Call the base Actor function
/// m_taskMgr.Update(); // Resume all active tasks once per tick
/// }
///
/// virtual void OnDestroy() override // Automatically called when this enemy leaves the scene
/// {
/// m_taskMgr.KillAllTasks(); // Kill all active tasks when we leave the scene
/// Actor::OnDestroy(); // Call the base Actor function
/// }
///
/// protected:
/// virtual Task<> ManageActor() // Overridden (in its entirety) by child classes
/// {
/// co_await WaitForever(); // Waits forever (doing nothing)
/// }
///
/// TaskManager m_taskMgr;
/// };
///
/// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
///
/// In the above example, TaskManager is instantiated once per high-level actor. It is updated once per frame within
/// the Tick() method, and all its tasks are killed when it leaves the scene in OnDestroy(). Lastly, a single entry-point
/// coroutine is run as a managed task when the actor enters the scene. (The above is the conventional method of integration
/// into this style of game engine.)
///
/// Note that it is sometimes necessary to have multiple TaskManagers within a single actor. For example, if there are
/// multiple tick functions (such as one for pre-physics updates and one for post-physics updates), then instantiating
/// a second "post-physics" task manager may be desirable.
#include <vector>
#include "Task.h"
NAMESPACE_SQUID_BEGIN
//--- TaskManager ---//
/// Manager that runs and resumes a collection of tasks.
class TaskManager
{
public:
~TaskManager() {} /// Destructor (disables copy/move construction + assignment)
/// @brief Run an unmanaged task
/// @details Run() return a @ref TaskHandle<> that holds a strong reference to the task. If there are ever no
/// strong references remaining to an unmanaged task, it will immediately be killed and removed from the manager.
template <typename tRet>
SQUID_NODISCARD TaskHandle<tRet> Run(Task<tRet>&& in_task)
{
// Run unmanaged task
TaskHandle<tRet> taskHandle = in_task;
WeakTask weakTask = std::move(in_task);
RunWeakTask(std::move(weakTask));
return taskHandle;
}
template <typename tRet>
SQUID_NODISCARD TaskHandle<tRet> Run(const Task<tRet>& in_task) /// @private Illegal copy implementation
{
static_assert(static_false<tRet>::value, "Cannot run an unmanaged task by copy (try Run(std::move(task)))");
return {};
}
/// @brief Run a managed task
/// @details RunManaged() return a @ref WeakTaskHandle, meaning it can be used to run a "fire-and-forget" background
/// task in situations where it is not necessary to observe or control task lifetime.
template <typename tRet>
WeakTaskHandle RunManaged(Task<tRet>&& in_task)
{
// Run managed task
WeakTaskHandle weakTaskHandle = in_task;
m_strongRefs.push_back(Run(std::move(in_task)));
return weakTaskHandle;
}
template <typename tRet>
WeakTaskHandle RunManaged(const Task<tRet>& in_task) /// @private Illegal copy implementation
{
static_assert(static_false<tRet>::value, "Cannot run a managed task by copy (try RunManaged(std::move(task)))");
return {};
}
/// @brief Run a weak task
/// @details RunWeakTask() runs a WeakTask. The caller is assumed to have already created a strong TaskHandle<> that
/// references the WeakTask, thus keeping it from being killed. When the last strong reference to the WeakTask is
/// destroyed, the task will immediately be killed and removed from the manager.
void RunWeakTask(WeakTask&& in_task)
{
// Run unmanaged task
m_tasks.push_back(std::move(in_task));
}
/// Call Task::Kill() on all tasks (managed + unmanaged)
void KillAllTasks()
{
m_tasks.clear(); // Destroying all the weak tasks implicitly destroys all internal tasks
// No need to call Kill() on each TaskHandle in m_strongRefs
m_strongRefs.clear(); // Handles in the strong refs array only ever point to tasks in the now-cleared m_tasks array
}
/// @brief Issue a stop request using @ref Task::RequestStop() on all active tasks (managed and unmanaged)
/// @details Returns a new awaiter task that will wait until all those tasks have all terminated.
Task<> StopAllTasks()
{
// Request stop on all tasks
std::vector<WeakTaskHandle> weakHandles;
for(auto& task : m_tasks)
{
task.RequestStop();
weakHandles.push_back(task);
}
// Return a fence task that waits until all stopped tasks are complete
return [](std::vector<WeakTaskHandle> in_weakHandles) -> Task<> {
TASK_NAME("StopAllTasks() Fence Task");
for(const auto& weakHandle : in_weakHandles)
{
co_await weakHandle; // Wait until task is complete
}
}(std::move(weakHandles));
}
/// Call @ref Task::Resume() on all active tasks exactly once (managed + unmanaged)
void Update()
{
// Resume all tasks
size_t writeIdx = 0;
for(size_t readIdx = 0; readIdx < m_tasks.size(); ++readIdx)
{
if(m_tasks[readIdx].Resume() != eTaskStatus::Done)
{
if(writeIdx != readIdx)
{
m_tasks[writeIdx] = std::move(m_tasks[readIdx]);
}
++writeIdx;
}
}
m_tasks.resize(writeIdx);
// Prune strong tasks that are done
auto removeIt = m_strongRefs.erase(std::remove_if(m_strongRefs.begin(), m_strongRefs.end(), [](const auto& in_taskHandle) {
return in_taskHandle.IsDone();
}), m_strongRefs.end());
}
/// Get a debug string containing a list of all active tasks
std::string GetDebugString(std::optional<TaskDebugStackFormatter> in_formatter = {}) const
{
std::string debugStr;
for(const auto& task : m_tasks)
{
if(!task.IsDone())
{
if(debugStr.size())
{
debugStr += '\n';
}
debugStr += task.GetDebugStack(in_formatter);
}
}
return debugStr;
}
private:
std::vector<WeakTask> m_tasks;
std::vector<TaskHandle<>> m_strongRefs;
};
NAMESPACE_SQUID_END
///@} end of TaskManager group