事件与信号¶
操作员界面、上位机或设备回调线程会在任意时刻发来指令。事件通道把这些指令变成一个节律:每条指令是一个周期。只需要知道"发生过"的通知(例如刷新面板)用信号,两次周期之间的多次触发合并成一次。
完整程序¶
操作员线程在 Tokio 运行时之外依次发送四条指令,每发一条就按一下刷新。System 里的指令域执行指令,面板域在收到刷新时显示最新位置。
use std::thread;
use roplat::comm::{Latest, Publisher, Subscriber};
use roplat::rhythm::{EventRhythm, SignalRhythm, create_event_channel, create_signal};
use roplat::{Lifecycle, Node, RoplatError, RoplatResult};
/// 操作员发来的指令。
#[derive(Clone, Copy, Debug)]
enum Command {
Goto(f64),
Home,
}
/// 单轴模拟机械臂。
#[derive(Default)]
struct Arm {
position: f64,
}
impl Lifecycle for Arm {
type Error = RoplatError;
}
/// 执行一条指令,并把机械臂的新位置发布给面板。
struct Execute {
board: Publisher<f64>,
done: usize,
}
impl Lifecycle for Execute {
type Error = RoplatError;
}
impl Node for Execute {
type Resources = (Arm,);
type Input = Command;
type Output = ();
async fn process<'call>(&'call mut self, command: Command, (arm,): (&'call mut Arm,)) {
arm.position = match command {
Command::Goto(target) => target,
Command::Home => 0.0,
};
self.done += 1;
self.board.publish(arm.position);
}
}
/// 操作面板:收到刷新信号时显示最新位置。
struct Panel {
board: Subscriber<f64>,
refreshes: usize,
}
impl Lifecycle for Panel {
type Error = RoplatError;
}
impl Node for Panel {
type Resources = ();
type Input = ();
type Output = ();
async fn process(&mut self, (): (), _: ()) {
self.refreshes += 1;
let _shown = self.board.latest().copied();
}
}
#[roplat::system]
async fn serve(
mut commands: EventRhythm<Command>,
mut refresh: SignalRhythm,
) -> RoplatResult<(f64, usize, usize)> {
let board = Latest::<f64>::new(1);
let mut arm = Arm::default();
let mut execute = Execute {
board: board.publisher(),
done: 0,
};
let mut panel = Panel {
board: board.subscriber(0),
refreshes: 0,
};
// 每条指令是指令域的一个周期。
commands
>> |command| {
&mut arm >> execute.resources.0;
command >> execute;
};
// 刷新请求合并:两次周期之间的多次触发只算一次。
refresh >> {
panel;
};
Ok((arm.position, execute.done, panel.refreshes))
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> RoplatResult<()> {
let (orders, commands) = create_event_channel::<Command>(4);
let (press, refresh) = create_signal();
// 操作员线程在 Tokio 之外:用 fire_blocking 等待队列空位。
let console = thread::spawn(move || {
for command in [
Command::Goto(0.4),
Command::Goto(-0.2),
Command::Home,
Command::Goto(0.7),
] {
orders.fire_blocking(command).expect("指令域已结束");
press.fire();
}
// 线程结束时两个触发器被 drop,两个节律随之结束。
});
let (position, done, refreshes) = serve(commands, refresh).await?;
console.join().expect("操作员线程 panic");
assert!((1..=4).contains(&refreshes));
println!("executed {done} commands, final position {position:.1}");
Ok(())
}
输出:
事件通道¶
create_event_channel::<Command>(4) 返回一对 (EventTrigger, EventRhythm),参数是队列容量。EventRhythm 交给 System 作为节律,每个事件的 Yield 就是指令本身:
触发器有三种发送方式,未能发送时都会把事件交回(FireError::Full 或 FireError::Closed):
| 方法 | 是否等待 | 用在哪里 |
|---|---|---|
try_fire |
不等待 | 任何地方,包括节点和周期内 |
fire_async().await |
异步等待空位 | 异步代码里,前提是接收域能同时推进 |
fire_blocking |
让线程睡眠等空位 | 只用于 Tokio 运行时之外的线程(界面、设备回调);在节点、周期或 hard 节律里调用会 panic |
信号¶
create_signal() 返回 (SignalTrigger, SignalRhythm)。SignalTrigger::fire 不等待、不分配、不会 panic,触发器可以克隆。下一个周期开始前的多次触发合并成一个周期,周期不携带数据。本例按了四次刷新,面板可能只刷新一到四次,取决于两边的时序,所以程序只断言这个范围。
需要每个事件和它的内容时,用事件通道;只需要"有变化"时,用信号。
节律什么时候结束¶
只要还有一个触发器存活,事件节律和信号节律就会继续等待。所有触发器被 drop、队列取空之后,节律结束。本例中操作员线程结束时 orders 和 press 被 drop,两个域随之结束,System 正常返回。
不要把触发器交给同一 System 里的节点
节点要到 System 结束才会被 drop,而 System 又在等这个节律结束,于是永远不会结束。触发器应留在 System 之外(另一个线程、另一部分程序),发送完就 drop;需要中途结束整个 System 时,用停止上下文。
与图内分支组合¶
事件周期里可以照常使用 match、守卫、图内 if 和 retry,写法与分支、条件与重试相同:指令进入一个周期,周期里按指令类型分支、检查、执行。
下一篇:hard 节律。