让我们从传统的顺序编程范例的工作开始。假设你想让嵌入式主板上的LED闪烁。一个常见的解决方案是将写一个程序是这样的(例如,参见Arduino Blink tutorial):
while (1) { /* RTOS task or a "superloop" */
turn_LED_on(); /* turn the LED on (computation) */
delay(500); /* wait for 500 ms (polling or blocking) */
turn_LED_off(); /* turn the LED off (computation) */
delay(1000); /* wait for 1000 ms (polling or blocking) */
}
这里的关键点是delay()
函数,它等待在线直到延迟为止。这个等待被称为“阻塞”,因为呼叫程序被阻止,直到delay()
返回。
请注意的Blinky程序调用两个delay()
不同的上下文:turn_LED_on()
之后的第一时间和turn_LED_off()
后的第二次。每次,delay()
都返回到代码中的不同位置。这意味着,当程序被阻止时,代码中的地点信息(呼叫的上下文)会自动保存。
Blinky程序非常简单,但原则上阻塞函数,如delay()
,可以从其他函数调用,每个函数都有 复杂的if-else-while代码。但是,delay()
将能够返回到调用的确切点,因为C编程语言保留了调用的上下文(在调用堆栈和程序计数器中)。
但阻止使整个程序对任何其他事件没有响应,因此人们想出了event-driven programming。
事件驱动程序围绕event-loop构建。一个例子事件驱动的代码看起来是这样的:
while (1) { /* event-loop */
Event *e = queue_get(); /* block when event queue is empty */
dispatch(e); /* handle the event, cannot block! */
}
主要的一点是,dispatch()
“事件处理程序”功能不能呼叫阻塞函数像delay()
。相反,dispatch()
只能执行一些即时操作,并且必须快速将返回到事件循环。这样,事件循环始终保持响应。
但是,通过返回dispatch()
函数从调用堆栈中删除自己的堆栈帧。所以与调用dispatch()
相关联的调用堆栈和程序计数器总是相同的,并且无法“记住”执行上下文。
相反,要使LED闪烁,dispatch()
功能必须依赖一些记忆LED状态(开/关)的变量(state
)。你怎么能写这样dispatch()
功能的示例如下:
static enum {OFF, ON } state = OFF; /* start in the OFF state */
timer_arm(1000); /* arm a timer to generate TIMEOUT event in 1000 ms */
void dispatch(Event *e) {
switch (state) {
case OFF:
if (e->sig == TIMEOUT) {
turn_LED_on();
timer_arm(500);
state = ON; /* transition to "ON" state */
}
break;
case ON:
if (e->sig == TIMEOUT) {
turn_LED_off();
timer_arm(1000);
state = OFF; /* transition to "OFF" state */
}
break;
}
}
我希望你能看到dispatch()
实现与ON和OFF由一个事件驱动TIMEOUT状态的state machine。