forked from chiyu1468/AirTrapMine
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
7.0 KiB
7.0 KiB
執行緒安全性實現 - 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 (布林鎖)
# 在 __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()中調用
- 這些方法只在 UI 執行緒的
-
讀取: 在
_update_attitude_ui()和_update_hud_ui()中讀取- 這些方法只在 UI 執行緒的
_process_ui_updates()中調用
- 這些方法只在 UI 執行緒的
# 只在 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)
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)
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 更新
# 所有 _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 中的 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
測試建議
- 頻率監控: 執行應用程序並查看列印的 ADI 更新頻率
- 視覺檢查: 觀察 ADI 指標的平滑性和應答性
- 壓力測試: 同時連接多個無人機並監控 CPU 使用率
- QDebug: 在時間敏感的部分添加
print()以測量執行時間
更新日期: 2025-01-20
版本: 執行緒安全優化 v1.0