第一版完成後,我很開心地按下「立即刷新」,結果頁面轉了好幾秒,看起來就像當機。原因很單純:程式正在逐台連線、檢查網站、讀取 RDP 狀態,瀏覽器只能原地等待。只有幾台機器時還能忍受,但設備繼續增加後,手動刷新與同步等待一定撐不住。
所以 HeadlessRuntime 負責看時間,並把慢工作交給背景執行緒。輪詢、RDP、網站與報表各自有 lock:同一件事還沒完成,就不重複開第二次;不同工作則能同時進行。按下立即刷新時,請求先進 queue,等收集完成再回覆。
我一開始以為多開執行緒就好,後來才明白重點是避免重複工作。_start_poll 和 _collect_refresh_requests 是這個轉折的核心。
def _start_poll(self):
if not self._poll_lock.acquire(False):
return
threading.Thread(target=self._poll, daemon=True).start()
def _poll(self):
try:
self.monitor.poll()
finally:
self._poll_lock.release()
finally 是關鍵;少了它,一次例外就可能讓排程永久停止。
實際專案還會保存結果並通知等待刷新者;這段特別留下最容易造成排程卡死的鎖定生命週期。
runtime._start_poll() # 成功取得鎖,啟動背景工作
runtime._start_poll() # 前一輪尚未完成,直接返回
reply = queue.Queue()
runtime.refresh_queue.put(reply)
result = reply.get(timeout=30)
print(result["status"])
範例同時呈現排程防重入與使用者等待立即刷新的兩條路徑。
曾經有兩位使用者同時按下刷新,系統便啟動兩輪主機掃描,不但變慢,還同時寫入快照。修復時我替每類背景工作加入獨立 lock,執行中的輪詢不再重入;刷新請求則放入 queue,共同等待同一輪結果。另一個 Bug 是工作拋出例外後 lock 沒被釋放,導致排程永久停止,因此釋放動作必須放在 finally,確保成功或失敗都能進入下一輪。
今天我學到,多開執行緒不是答案,工作生命週期與重入控制才是關鍵。下一篇會保存快照與歷史,讓系統開始理解過去。