# 執行緒安全性實現 - GCS GUI ## 架構概述 GCS GUI 採用 **拉取式 UI 更新架構** 以確保執行緒安全和高效的UI渲染,避免UI執行緒被資料收集堵塞。 ## 執行緒模型 ### 1. **主 GUI 執行緒** (Qt Event Loop) - 負責所有 UI 操作(更新標籤、表格、地圖、ADI 等) - 執行 `_process_ui_updates()` 每 33ms(30Hz) ### 2. **ROS 執行緒** (Single-threaded Executor) - 執行 `spin_ros()` 定時器每 10ms - 只負責收集資料並快取,**不進行任何 UI 操作** ### 3. **背景接收執行緒** (Receiver Threads) - UDP/WebSocket/Serial 接收器在獨立執行緒上運行 - 寫入 `monitor.latest_data` 字典(快取層) ### 4. **任務執行執行緒** (Mission Executor) - 運行在獨立執行緒上 - 不直接訪問 UI 元素 ## 資料流 ``` 接收執行緒 ↓ monitor.latest_data (共享字典) ↓ spin_ros() [UI 執行緒, 10ms] ↓ (快取資料,不更新 UI) _ui_update_cache (字典) ↓ _process_ui_updates() [UI 執行緒, 30Hz] ↓ (批次處理,更新 UI) DronePanel, OverviewTable, DroneMap ``` ## 執行緒安全機制 ### 1. **_ui_cache_lock (布林鎖)** ```python # 在 __init__ 中初始化 self._ui_cache_lock = False self._ui_update_cache = {} # 在 spin_ros() 中快取資料 self._ui_update_cache[drone_id][msg_type] = data # 在 _process_ui_updates() 中保護快取訪問 if self._ui_cache_lock or not self._ui_update_cache: return self._ui_cache_lock = True try: # 處理快取資料 for drone_id in list(self._ui_update_cache.keys()): ... finally: self._ui_cache_lock = False ``` ### 2. **drone_positions 與 drone_headings 訪問** - **寫入**: 在 `_update_gps_ui()` 和 `_update_hud_ui()` 中進行 - 這些方法**只在 UI 執行緒**的 `_process_ui_updates()` 中調用 - **讀取**: 在 `_update_attitude_ui()` 和 `_update_hud_ui()` 中讀取 - 這些方法**只在 UI 執行緒**的 `_process_ui_updates()` 中調用 ```python # 只在 UI 執行緒上讀寫 heading = self.drone_headings.get(drone_id, 0) # 讀取 self.drone_headings[drone_id] = heading # 寫入 ``` ### 3. **資料同步屬性** | 屬性 | 寫入執行緒 | 讀取執行緒 | 保護機制 | |------|----------|----------|--------| | `_ui_update_cache` | spin_ros (UI) | _process_ui_updates (UI) | _ui_cache_lock | | `monitor.latest_data` | 接收執行緒 | spin_ros (UI) | 直接讀取後清空 | | `drone_positions` | _update_gps_ui (UI) | _update_hud_ui (UI) | 同一執行緒 | | `drone_headings` | _update_hud_ui (UI) | _update_attitude_ui (UI) | 同一執行緒 | | `self.drones[*]` | add_drone (UI) | 各 _update_*_ui (UI) | 同一執行緒 | ## UI 更新流程 ### 步驟 1: 資料收集 (spin_ros, 10ms) ```python def spin_ros(self): # 執行 ROS 收集資料 self.executor.spin_once(timeout_sec=0.01) # 只快取資料,不更新 UI for (drone_id, msg_type), data in self.monitor.latest_data.items(): if drone_id not in self._ui_update_cache: self._ui_update_cache[drone_id] = {} self._ui_update_cache[drone_id][msg_type] = data self.monitor.latest_data.clear() ``` ### 步驟 2: 批次 UI 更新 (_process_ui_updates, 30Hz / 33ms) ```python def _process_ui_updates(self): # 檢查是否有資料且無鎖定 if self._ui_cache_lock or not self._ui_update_cache: return self._ui_cache_lock = True # 上鎖 try: # 批次處理各無人機的快取資料 for drone_id in list(self._ui_update_cache.keys()): data_dict = self._ui_update_cache.get(drone_id, {}) # 依訊息類型調用相應的 UI 更新方法 if 'attitude' in data_dict: self._update_attitude_ui(drone_id, data_dict['attitude']) if 'hud' in data_dict: self._update_hud_ui(drone_id, data_dict['hud']) # ... 其他訊息類型 # 清空快取 self._ui_update_cache.clear() finally: self._ui_cache_lock = False # 解鎖 ``` ### 步驟 3: 各資料類型的 UI 更新 ```python # 所有 _update_*_ui 方法都在 UI 執行緒上運行 def _update_gps_ui(self, drone_id, data): # 安全地寫入共享結構(同一執行緒) self.drone_positions[drone_id] = (lat, lon) # 更新 UI 元素 self.update_overview_table(drone_id, 'latitude', ...) self.drone_map.update_drone_position(drone_id, lat, lon, heading) def _update_hud_ui(self, drone_id, data): # 安全地寫入共享結構(同一執行緒) self.drone_headings[drone_id] = heading # 讀取之前寫入的資料(同一執行緒,無競賽條件) if drone_id in self.drone_positions: lat, lon = self.drone_positions[drone_id] self.drone_map.update_drone_position(drone_id, lat, lon, heading) ``` ## 關鍵設計原則 ### 1. **執行緒隔離** - 所有 UI 操作都在主 GUI 執行緒上進行 - ROS 資料收集隔離在單獨的定時器回調中 - 背景執行緒只寫入快取,不觸及 UI ### 2. **資料流向一致** ``` 背景執行緒 → monitor.latest_data ↓ 主 UI 執行緒 (spin_ros) ↓ _ui_update_cache ↓ 主 UI 執行緒 (_process_ui_updates) ↓ UI 元素 ``` ### 3. **批次更新減少開銷** - 不是每次收到訊息就更新 UI(造成主執行緒壓力) - 而是批次收集 33ms 內的所有更新,一次性渲染 - 結果:UI 流暢度提高,執行緒爭奪減少 ### 4. **簡單的同步策略** - 使用簡單的布林標誌而不是複雜的鎖 - 避免死鎖,因為只有一個重要的臨界區 (`_ui_update_cache`) ## 監控與驗證 ### 態度指示器(ADI)更新頻率 在 [drone_panel.py](drone_panel.py) 中的 `update_attitude()` 方法自動列印頻率: ``` [drone_0] Attitude update frequency: 30.00 Hz [drone_1] Attitude update frequency: 29.98 Hz ``` ### 預期頻率 - **資料收集**: 10ms = 100Hz(但不更新 UI) - **UI 渲染**: 30Hz(批次模式) - **ADI 更新**: 約 30Hz(受限於 `_process_ui_updates()` 頻率) ## 故障排除 ### 1. ADI 仍然更新頻率低 - 檢查 `ui_update_timer` 是否啟動 - 確認 `_process_ui_updates()` 正在被調用 - 檢查是否有其他執行緒在嘗試直接更新 UI ### 2. 地圖或表格更新時卡頓 - 檢查 `drone_map.update_drone_position()` 和 `update_overview_table()` 是否有阻塞操作 - 確認這些操作在 `_process_ui_updates()` 中被調用(主執行緒) ### 3. 資料不同步 - 驗證 `_ui_cache_lock` 是否正確保護快取訪問 - 檢查是否有代碼在 spin_ros 之外寫入 `_ui_update_cache` ## 測試建議 1. **頻率監控**: 執行應用程序並查看列印的 ADI 更新頻率 2. **視覺檢查**: 觀察 ADI 指標的平滑性和應答性 3. **壓力測試**: 同時連接多個無人機並監控 CPU 使用率 4. **QDebug**: 在時間敏感的部分添加 `print()` 以測量執行時間 --- **更新日期**: 2025-01-20 **版本**: 執行緒安全優化 v1.0