协作式调度器 (cooperative scheduler)
目录
简介
Process 是一种有用的工具,可用于绕过 system 的严格定义,并以不同的方式引入逻辑,通常无需借助其他 component 类型。
EnTT 通过引入几个用于定义和执行协作式 process 的类,为这种范式提供了最小限度的支持。
Process
典型的 task 继承自 process 类模板。派生类还需指定经过时间 (elapsed times) 的预期类型。
Process 应根据需要实现以下成员函数(请注意,除非派生类想要 覆盖 (override) 默认行为,否则并非必须定义这些函数):
-
void update(Delta, void *);每个 tick 调用一次,直到 process 被显式中止 (aborted) 或结束(无论是否有错误)。每个 process 至少应定义它以 正常 工作。
void *参数是指向用户数据(如果有)的不透明指针,在 update 期间直接转发给 process。 -
void succeeded();在成功的情况下调用,紧跟在 update 之后并在同一个 tick 内。
-
void failed();在发生错误的情况下调用,紧跟在 update 之后并在同一个 tick 内。
-
void aborted();仅当 process 被显式中止时调用。不保证它在同一个 tick 内执行。这完全取决于 process 是否被立即中止。
类还可以通过调用 succeed 和 fail,以及 pause 和 unpause process 本身来更改 process 的状态。
所有这些都是公开可用的成员函数,用于轻松管理 process 的生命周期。
出于好奇,这里提供一个最小示例:
struct my_process: entt::process {
using allocator_type = entt::process::allocator_type;
using delta_type = entt::process::delta_type;
my_process(const allocator_type &allocator, delta_type delay)
: entt::process{allocator},
remaining{delay}
{}
void update(delta_type delta, void *) {
remaining -= std::min(remaining, delta);
// ...
if(!remaining) {
succeed();
}
}
private:
delta_type remaining;
};
Continuation
Process 在成功终止后可以跟随其他 process。
这种配对可以在创建时设置,使 process 在概念上彼此独立,同时在运行时将它们组合起来:
my_process process{};
process.then<my_other_process>();
这种方法允许独立开发 process,并将它们组合起来以定义复杂的动作。
例如,延迟操作,其中父 process(如计时器)在时间结束后 调度 (schedule) 子 process(延迟的 task)。
then 函数也接受 lambda,它们在内部与专用的 process 关联:
process.then([](entt::process &proc, std::uint32_t delta, void *data) {
// ...
})
Lambda 函数接受对管理它的 process 的引用(以便能够终止它、暂停它等),加上通常也传递给 update 函数的值。
共享 Process
所有 process 都继承自 std::enable_shared_from_this,以允许与调用者共享。
返回的 smart pointer 是使用与 scheduler 关联的 allocator 创建的,因此也适用于其所有 process。可以通过在 process 本身上调用 get_allocator 来获取这同一个 allocator。
尽可能而言,共享 process 的目的不是为了让调用者管理它。这实际上可能会损害 scheduler 和 process 本身的正常运行。
相反,其目的是允许调用者保存对 process 的有效引用,从而允许他们通过诸如 pause 等调用来干预其生命周期。
Scheduler
协作式 scheduler 运行不同的 process 并帮助管理它们的生命周期。
每个 process 每个 tick 调用一次。如果它终止,则会自动从 scheduler 中移除,并且永远不会再次被调用。否则,它是下一个 tick 再次运行的良好候选者。
Process 也可以有 子节点 (child)。在这种情况下,当父 process 终止时,仅当它成功返回时,才会被其子节点替换。如果发生错误,父 process 及其子节点都会被丢弃。这样,很容易创建一个 process 链 (chain of processes) 来顺序运行。
使用 scheduler 非常简单。要创建它,用户必须仅提供经过时间的类型,完全不需要任何参数:
entt::basic_scheduler<std::uint64_t> scheduler;
否则,对于最常见的情况,也可以使用 scheduler 别名。它使用 std::uint32_t 作为默认类型:
entt::scheduler scheduler{};
该类具有查询其内部数据结构的成员函数,如 empty 或 size,以及一个将其重置为干净状态的 clear 实用工具:
// 检查是否还有正在运行的 process
const auto empty = scheduler.empty();
// 获取仍在运行的 process 数量
entt::scheduler::size_type size = scheduler.size();
// 将 scheduler 重置为其初始状态并丢弃所有 process
scheduler.clear();
要将 process 附加到 scheduler,请使用 process 类型和用于构造它的参数调用 attach 函数:
scheduler.attach<my_process>(_1000u);
Scheduler 还将为其 process 提供其 allocator 作为第一个参数。
对于 lambda 或 functor,所需的签名与 process 的 then 函数中已经看到的签名相同:
scheduler.attach([](entt::process &, std::uint32_t, void *){ /* ... */ });
在这两种情况下,新创建的 process 都通过引用返回,并且其 then 成员函数用于创建顺序运行的 process 链。
作为一个最小使用示例:
// 以 lambda 函数的形式调度一个 task
scheduler.attach([](entt::process &, std::uint32_t, void *) {
// ...
})
// 以另一个 lambda 函数的形式追加一个子节点
.then([](entt::process &, std::uint32_t, void *) {
// ...
})
// 以 process 类的形式追加一个子节点
.then<my_process>(1000u);
要更新 scheduler 并因此更新其所有 process,应使用 update 成员函数:
// 更新所有 process,不提供用户数据
scheduler.update(delta);
// 更新所有 process 并为它们提供自定义数据
scheduler.update(delta, &data);
除了这些函数之外,scheduler 还提供一个 abort 成员函数,用于一次性丢弃所有正在运行的 process:
// 突然中止所有 process ...
scheduler.abort(true);
// ... 或在下一个 tick 优雅地中止
scheduler.abort();
传递给 abort 函数的参数指示是应立即停止执行,还是应在下一个 tick 通知 process。