Using Coroutines / 使用协程

PixelUI 的 Coroutine 是固定 context 加宏的协作式状态机。它适合编排这类短流程

  • 带有先后顺序的启动动画
  • 需要非阻塞延时的短流程

一个例子

1
2
3
4
5
6
7
8
9
10
11
12
Coroutine intro_{[this](CoroutineContext& ctx) {
CORO_BEGIN(ctx);
CORO_DELAY(ctx, m_ui, 160, 100);
btn_light.onLoad(); // 先播放btn_light的进场动画
m_ui.animate(anim_x2_clip_light, 81, 950, EasingType::EASE_OUT_CUBIC, PROTECTION::PROTECTED);
m_ui.animate(anim_x_water_pump, 25, 350, EasingType::EASE_OUT_CUBIC, PROTECTION::PROTECTED);
brace_plant.onLoad();
CORO_DELAY(ctx, m_ui, 200, 200); // 挂起协程 200ms
m_ui.animate(anim_x2_clip_soil, 57, 650, EasingType::EASE_OUT_CUBIC, PROTECTION::PROTECTED);
CORO_DELAY(ctx, m_ui, 200, 300);
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_);
}

API 详解

使用这些 API 需要包含:

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

01 - 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 外,不要手动改写 pcstatewaitUntilwaitReason,影响协程调度器

02 - Coroutine 对象

构造函数

1
Coroutine(CoroutineFunction func);

CoroutineFunction 的实际类型是:

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

具体上限见资源上限

start()

1
void start();

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

reset()

1
void reset();

将状态恢复为 CREATED,并重置 pcwaitUntilwaitReason

状态查询

1
2
3
bool isFinished() const;
CoroutineContext& getContext();
const CoroutineContext& getContext() const;
  • isFinished() 判断 context 是否为 FINISHED
  • getContext() 可用于读取状态或访问 localData。优先使用 const 版本进行诊断。

03 - 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_DELAY(ctx, ui, ms, line)

1
CORO_DELAY(ctx, ui_, ms, 2); // 挂起协程,等待ms毫秒后恢复

此宏记录 deadline(ui.getCurrentTime() + ms),将协程上下文状态设为 SUSPENDED

到期后从宏的下一行继续。deadline 使用无符号回绕安全比较,但单次等待必须小于 2^31 ms。它会参与 tickless 的最早唤醒时间计算。


CORO_WAIT_ANIMATION(ctx, ui, line)

1
CORO_WAIT_ANIMATION(ctx, ui_, 3);

挂起直到 ui.activeAnimationCount()( PixelUI的全部活跃动画 )变为 0。

若其他模块持续创建动画,等待会相应延长。


CORO_YIELD(ctx, line)

用途为让出一帧,实际上基本用不到,是 Duff’s device 风格协程的一个基础原语,调配粒度太粗。基本功能都可以使用 CORO_DELAY 等进行实现

1
CORO_YIELD(ctx, 1); // 在不进入定时等待的情况下,保存执行位置并立即返回。

coroutine 仍为 RUNNING,所以 scheduler 会在下一次处理机会继续执行,而不是在同一次函数调用中继续。它适合把较长流程主动拆成多个短步骤,不能代替基于真实时间的延时。


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); // 将 coroutine 注册到协程管理器

把 coroutine 的非拥有指针加入 scheduler。此操作不会调用 start(),也不会检查重复注册。调用前应先 reset()/start(),并确保同一个指针只注册一次。

容量由 PIXELUI_MAX_COROUTINE_NUM 决定;接口没有失败返回值,因此调用方必须在设计上保证不超过固定容量。

对象本身必须活到被移除或 scheduler 清理之后。所以一定要把 Coroutine 作为 App 成员。


removeCoroutine()

1
void removeCoroutine(Coroutine* coroutine); // 将 coroutine 移出协程管理器

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

clearAllCoroutines()


1
void clearAllCoroutines();

清空当前 PixelUI scheduler 中的全部非拥有指针,此操作不会 reset 注册过的协程的上下文。

协程被视为”非持有引用”(non-owning reference),ViewManager 在销毁 App 前会执行全局清理,以避免遗留悬空指针,在 app 切换时统一清理。每个 app 应该在 onEnter() / onResume() 中重新注册自己需要的协程


getActiveCoroutineCount()

1
size_t getActiveCoroutineCount();

返回 scheduler 当前保存的注册项数量,包括

  • 等待中的 coroutine
  • 被注册但尚未 start()CREATED coroutine。

它是注册数量,完成的 coroutine 会在 scheduler 更新中自动移除。


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;

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