經過前幾天的奮戰,我們的 EdgeNode 已經具備了強大的檔案處理與 USB 通訊能力。它能在背景默默地攔截資料、切換磁碟、並用軟體模擬重插拔。
但是,對於第一線的醫療人員或使用者來說,他們根本看不到這些在 Linux 核心中翻騰的位元與指令。他們唯一能感知到這台無頭設備 (Headless Device) 當下狀態的途徑,只有機器外殼上的那顆 RGB LED 指示燈。
聽起來很簡單對吧?只要在程式碼裡呼叫 GPIO.output() 就好了。但這正是災難的開始。
在一個具備一定複雜度的系統中,我們的 Python 應用程式往往是多執行緒 (Multi-threading) 的架構。
SyncEngine 執行緒在監控 USB 的插拔。SerialWorker 執行緒在苦苦等待 UART 傳來的感測器資料。當設備開機完成,主程式說:「亮綠燈呼吸」。
突然,UART 收到資料了,SerialWorker 說:「快!改閃藍燈」。
此時,底層的磁碟空間滿了,SyncEngine 說:「不對!亮紅燈警告!」
如果你只是單純在各個執行緒裡面寫:
def on_data_receive():
# 藍燈閃爍
while data_transferring:
set_color(BLUE)
time.sleep(0.5)
set_color(BLACK)
time.sleep(0.5)
當多個執行緒同時執行這類包含了 time.sleep() 的動畫迴圈,並且直接操作共享的硬體資源 (GPIO pin) 時,你會看到世界上最醜的燈號:混色閃爍。
藍燈剛亮起,另一個執行緒把綠燈也點亮了,於是燈號變成了青色 (Cyan)。接著紅燈執行緒強行介入關閉了所有燈,LED 瘋狂地閃爍著不規則的雜訊。
[!WARNING]
這不僅僅是「難看」而已,在醫療或工業設備中,錯誤的燈號會導致嚴重的操作失誤。使用者可能以為資料已經傳輸完畢拔除設備,進而導致資料損毀。
為了解決這個問題,你可能會直覺地加上 Threading Lock (互斥鎖):
led_lock = threading.Lock()
def on_data_receive():
with led_lock:
while data_transferring:
set_color(BLUE)
time.sleep(0.5)
set_color(BLACK)
time.sleep(0.5)
這樣確實解決了混色的問題,但產生了另一個更致命的狀況:阻塞 (Blocking)。
如果 data_transferring 持續了十分鐘,這十分鐘內 led_lock 都被死死扣住。如果這時系統發生了過熱或磁碟毀損的「致命錯誤」,紅燈警報根本亮不起來,因為它在等待 led_lock 釋放!
為什麼傳統的寫法會這麼痛苦?因為 Python 的 threading (或是說背後 Linux 作業系統的 CPU 排程器) 的設計宗旨在於 「公平地分配 CPU 時間給每一個執行緒」。
排程器並不知道你的「紅燈」代表的是生死交關的致命錯誤,也不知道你的「綠燈」只是無關緊要的待機動畫。在作業系統眼中,它們只是兩段都需要被執行的程式碼。當你呼叫 time.sleep(0.5) 時,作業系統就開心地把 CPU 讓給其他執行緒去弄亂你的 GPIO。
我們必須認知到:控制硬體狀態,不能依賴通用的 CPU 排程器,也不能直接在業務邏輯層呼叫硬體 API。
為了解決這場大亂鬥,我們需要將「發布指令」與「執行指令」徹底解耦。
這就是我們明天要介紹的主題:軟即時架構 (Soft Real-Time) 與搶佔式任務排程。我們將親手用 Python 打造一個能秒殺上述所有問題的燈號控制引擎。敬請期待 Day 21!