EnTT: 协作调度 Cooperative Scheduler

记录 EnTT 协作式调度器中的 process、scheduler、then 链式调度与更新流程。

对于一个复杂的大型系统来说,将逻辑划分成更小的流程环节一般来说是更好的实践,如此一来,就涉及到如何对这些 流程 进行调度和协作的问题

流程 Process

首先我们要定义 流程, entt::process 的用法是 CRTP 模版的典型用法

它支持覆盖以下方法

  • void update(Delta, void *);
    该方法会每个 tick 调用一次,直到 process 明确中止或成功执行, 其中 void * 表示用户数据指针
  • void init();
    流程 加入 调度器 的运行队列时,本方法会被调用一次, 这通常 发生在 流程 被附加到调度器并且 流程顶层流程 时, 如果 它是 子流程, 则将在其取代 父流程 时发生调用
  • void succeeded();
    此方法会在 流程 成功执行后被调用
  • void failed();
    此方法会在 流程 执行失败后被调用
  • void aborted();
    此方法在 流程 被显式中止时被调用, 它可以被 调度器abort() 触发

流程 本身也可以通过直接调用上述方法来改变自身状态, 除了上面的 方法之外, 还有 pauseunpause 方法

struct my_process: entt::process<my_process, std::uint32_t> {
    using delta_type = std::uint32_t;
 
    my_process(delta_type delay)
        : remaining{delay}
    {}
 
    void update(delta_type delta, void *) {
        remaining -= std::min(remaining, delta);
 
        // ...
 
        if(!remaining) {
            succeed();
        }
    }
 
private:
    delta_type remaining;
};

也可以使用 lambda 和 函子来作为 流程, 它们本身不被支持, 但是 它们被注册到 调度器 时, 会自动使用适配器, 这要求它们必须有以下签名

void(Delta delta, void *data, auto succeed, auto fail);
  • delta 是经过的时间
  • data 是自定义数据指针
  • succeed 是进程执行成功之后进行的回调
  • fail 是进程执行失败之后进行的回调

注意 succeedfail 都不接受任何参数

调度器 Scheduler

调度器会在每个 tick 调用所有注册的 流程, 如果 流程 被终结 则会从调度器中移除, 并不会在以后的 tick 中进行调用

entt::basic_scheduler<std::uint64_t> scheduler;
 
// 默认 delta type 是 std::uint32_t
entt::scheduler scheduler;

流程 可以拥有子流程, 在这种情况下只有当 父流程 执行成功以后, 子流程 才会进入调度器被执行, 如果 父流程 执行失败, 那么它和 它的 子流程 都会被丢弃, 这可以用于构造流程执行链

调度器拥有常见容器类似的方法

// checks if there are processes still running
const auto empty = scheduler.empty();
 
// gets the number of processes still running
entt::scheduler::size_type size = scheduler.size();
 
// resets the scheduler to its initial state and discards all the processes
scheduler.clear();

就像上面介绍 流程 所说的, 有两种附加方式

// 继承方式, 提供自定义数据
scheduler.attach<my_process>(1000u);
// lambda 方式, 会在内部自动使用适配器
scheduler.attach([](auto...){ /* ... */ });

不管使用哪种方式进行流程注册, 调度器都会返回自身, 这让我们可以使用 then 方法进行链式任务的注册

// schedules a task in the form of a lambda function
scheduler.attach([](auto delta, void *, auto succeed, auto fail) {
    // ...
})
// appends a child in the form of another lambda function
.then([](auto delta, void *, auto succeed, auto fail) {
    // ...
})
// appends a child in the form of a process class
.then<my_process>(1000u);

使用 update 方法对所有在 调度器 中注册的 流程 进行执行

// updates all the processes, no user data are provided
scheduler.update(delta);
 
// updates all the processes and provides them with custom data
scheduler.update(delta, &data);

使用 abort 方法立即或在下一个 tick 丢弃所有正在运行的 流程

// aborts all the processes abruptly ...
scheduler.abort(true);
 
// ... or gracefully during the next tick
scheduler.abort();