Using Coroutines / 使用协程

PixelUI 的 Coroutine 是固定 context 加宏的协作式状态机,不是 C++20 language coroutine。它适合编排“启动动画 -> 等待 -> 继续”这类短流程。

1
2
3
4
5
6
7
8
9
10
11
Coroutine intro_{[this](CoroutineContext& ctx) {
CORO_BEGIN(ctx);

ui_.animate(panelX_, 0, 300, EasingType::EASE_OUT_CUBIC);
CORO_WAIT_ANIMATION(ctx, ui_, 1);
CORO_DELAY(ctx, ui_, 250, 2);
ready_ = true;
ui_.markDirty();

CORO_END(ctx);
}};

注册与清理

1
2
3
4
5
6
7
8
9
10
void onEnter(ExitCallback cb) override {
IApplication::onEnter(cb);
intro_.reset();
intro_.start();
ui_.addCoroutine(&intro_);
}

void onExit() override {
ui_.removeCoroutine(&intro_);
}

Coroutine 通常应是 App 成员。对象状态、scheduler 注册、deadline 和恢复点是彼此独立的规则,下面按 API 说明。

API 详解

使用这些 API 需要包含:

1
#include "core/coroutine/Coroutine.h"

CoroutineContext

每个 Coroutine 都内置一个 context,用它保存跨多次调用的执行位置和等待状态:

1
2
3
4
5
6
7
struct CoroutineContext {
uint32_t pc;
uint32_t waitUntil;
uint32_t localData[8];
CoroutineState state;
CoroutineWaitReason waitReason;
};
字段 含义
pc 宏状态机的执行位置。由 CORO_* 宏维护,业务代码不要直接修改。
waitUntil CORO_DELAY 使用的绝对时间戳。由宏维护。
localData[8] 提供给业务代码的 8 个持久化 uint32_t 槽位,可在多次 resume 之间保存简单状态。
state 当前状态:CREATEDRUNNINGSUSPENDEDFINISHED
waitReason 挂起原因:NONEDELAYANIMATION。调度器据此计算下次唤醒时间。

reset() 不会清空 localData。如果一次新的流程不应继承旧值,需要在启动前显式清零所用槽位:

1
2
3
intro_.reset();
intro_.getContext().localData[0] = 0;
intro_.start();

localData 外,通常只读取 context 进行诊断,不要手动改写 pcstatewaitUntilwaitReason,否则宏状态和调度器判断可能不一致。

Coroutine

构造函数

1
Coroutine(CoroutineFunction func);

CoroutineFunction 的实际类型是:

1
etl::inplace_function<void(CoroutineContext&), CALLBACK_STORAGE_SIZE>

回调存放在对象内部,不使用堆。lambda 捕获超过配置容量会在编译期失败;具体上限见资源上限。构造只创建 coroutine,不会执行回调。

start()

1
void start();

仅当状态为 CREATED 时,将状态改为 RUNNING 并把执行位置设为开头。它不会立即调用 coroutine 函数;函数会在 scheduler 下一次更新时运行。对 RUNNINGSUSPENDEDFINISHED 状态调用没有效果。若要重新运行已完成的 coroutine,先调用 reset()

reset()

1
void reset();

将状态恢复为 CREATED,并重置 pcwaitUntilwaitReason。它不会清空 localData,也不会自动从 scheduler 移除或重新注册 coroutine。

状态查询

1
2
3
bool isFinished() const;
CoroutineContext& getContext();
const CoroutineContext& getContext() const;

isFinished() 只判断 context 是否为 FINISHEDgetContext() 可用于读取状态或访问 localData;优先使用 const 版本进行诊断。

低层调度接口

1
2
3
void resume(uint32_t currentTime, bool animationActive = false);
bool shouldRun(uint32_t currentTime, bool animationActive = false) const;
uint32_t nextWakeupMs(uint32_t currentTime, bool animationActive) const;

这些接口主要由 CoroutineScheduler 调用,普通 App 不需要手动驱动。resume() 只会执行 RUNNING 或已经满足恢复条件的 SUSPENDED coroutine;shouldRun() 判断当前是否可运行;nextWakeupMs() 返回距离下一次运行的毫秒数,暂时没有定时唤醒需求时返回 PixelUITime::NO_WAKEUP

手动调用时,currentTimeanimationActive 必须与同一 PixelUI 实例的真实状态一致,否则延时或动画等待语义会出错。

Coroutine 宏

所有宏都必须在同一个 coroutine 函数的 CORO_BEGIN(ctx)CORO_END(ctx) 之间使用。

CORO_BEGIN(ctx) / CORO_END(ctx)

1
2
3
CORO_BEGIN(ctx);
// coroutine body
CORO_END(ctx);

CORO_BEGIN 打开基于 ctx.pc 的状态机;CORO_END 将状态设为 FINISHED 并返回。正常执行路径不能绕过 CORO_END,否则 coroutine 会保持 RUNNING 并被继续调度。

CORO_YIELD(ctx, line)

1
CORO_YIELD(ctx, 1);

保存执行位置并立即返回,但不进入定时等待。coroutine 仍为 RUNNING,所以 scheduler 会在下一次处理机会继续执行,而不是在同一次函数调用中继续。它适合把较长流程主动拆成多个短步骤,不能代替基于真实时间的延时。

CORO_DELAY(ctx, ui, ms, line)

1
CORO_DELAY(ctx, ui_, 250U, 2);

ui.getCurrentTime() + ms 记录 deadline,将状态设为 SUSPENDED,到期后从宏的下一行继续。deadline 使用无符号回绕安全比较,但单次等待必须小于 2^31 ms。它会参与 tickless 的最早唤醒时间计算,不需要开启 continuous draw。

CORO_WAIT_ANIMATION(ctx, ui, line)

1
CORO_WAIT_ANIMATION(ctx, ui_, 3);

挂起到 ui.activeAnimationCount() 变为 0。这里观察的是该 PixelUI 的全部动画,不只是当前 App 或当前 coroutine 启动的动画。若其他模块持续创建动画,等待会相应延长。

line 参数规则

line 最终会成为 C++ switchcase 标签,因此必须是编译期整数常量,并且在同一个 coroutine 函数内唯一;0 已由 CORO_BEGIN 使用,不能再传。可以使用人工编号,也可以用 __LINE__ 自动取得当前源码行号:

1
CORO_DELAY(ctx, ui_, 100U, __LINE__);

由于恢复点是 case 标签,不要让需要跨恢复点存活的普通局部变量依赖栈上的初始化。把持久状态放在 App 成员或 ctx.localData 中:

1
2
3
4
5
6
7
8
9
Coroutine worker_{[this](CoroutineContext& ctx) {
CORO_BEGIN(ctx);

ctx.localData[0] = 0;
CORO_DELAY(ctx, ui_, 100U, 1);
++ctx.localData[0];

CORO_END(ctx);
}};

PixelUI 注册接口

addCoroutine()

1
void addCoroutine(Coroutine* coroutine);

把非拥有指针加入 scheduler;nullptr 会被忽略。它不会调用 start(),也不会检查重复注册。调用前应先 reset()/start(),并确保同一个指针只注册一次。容量由 PIXELUI_MAX_COROUTINE_NUM 决定;接口没有失败返回值,因此调用方必须在设计上保证不超过固定容量。

对象本身必须活到被移除或 scheduler 清理之后。最稳妥的做法是把 Coroutine 作为 App 成员,而不是函数局部变量。

removeCoroutine()

1
void removeCoroutine(Coroutine* coroutine);

移除所有与该指针相同的注册项。它不会改变 coroutine 自身的状态;之后可以先 reset()start(),再重新注册。传入未注册的指针不会产生效果。

clearAllCoroutines()

1
void clearAllCoroutines();

清空当前 PixelUI scheduler 中的全部非拥有指针,但不会 reset 对应对象。这个 API 影响所有 App/模块,不适合作为普通 App 的局部清理手段;退出时优先对自己的 coroutine 调用 removeCoroutine()。ViewManager 在销毁 App 前会执行全局清理,以避免遗留悬空指针。

getActiveCoroutineCount()

1
size_t getActiveCoroutineCount();

返回 scheduler 当前保存的注册项数量,包括等待中的 coroutine,以及被注册但尚未 start()CREATED coroutine。它是注册数量,不是“本帧正在执行”的数量。完成的 coroutine 会在 scheduler 更新中自动移除。

PIXELUI_USE_COROUTINE0 时,以上四个 PixelUI 接口仍然可以调用:注册、移除和清理操作为空操作,getActiveCoroutineCount() 固定返回 0。业务代码若直接声明 Coroutine,仍需确保对应头文件和构建配置一致。

CoroutineScheduler(框架级)

1
2
3
4
5
6
7
explicit CoroutineScheduler(PixelUI& ui);
void addCoroutine(Coroutine* coroutine);
void removeCoroutine(Coroutine* coroutine);
void update(uint32_t currentTime);
uint32_t nextWakeupMs(uint32_t currentTime) const;
void clear();
size_t getActiveCount() const;

PixelUI 已经持有并驱动 scheduler,App 通常不应自行构造或调用它。update() 会先清理已完成项,再依次恢复满足条件的 coroutine,并在本轮末尾再次清理刚完成的项。每个 coroutine 执行前都会重新读取全局动画状态,因此前一个 coroutine 新启动的动画会影响后一个 coroutine 的 CORO_WAIT_ANIMATION 判断。nextWakeupMs() 供 PixelUI 的 event-driven/tickless 调度计算最早唤醒时间。