讓我們從傳統的順序編程範例的工作開始。假設你想讓嵌入式主板上的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。