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

執行緒安全性實現 - GCS GUI

架構概述

GCS GUI 採用 拉取式 UI 更新架構 以確保執行緒安全和高效的UI渲染避免UI執行緒被資料收集堵塞。

執行緒模型

1. 主 GUI 執行緒 (Qt Event Loop)

  • 負責所有 UI 操作更新標籤、表格、地圖、ADI 等)
  • 執行 _process_ui_updates() 每 33ms30Hz

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() 中調用
  • 讀取: 在 _update_attitude_ui()_update_hud_ui() 中讀取

    • 這些方法只在 UI 執行緒_process_ui_updates() 中調用
# 只在 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

測試建議

  1. 頻率監控: 執行應用程序並查看列印的 ADI 更新頻率
  2. 視覺檢查: 觀察 ADI 指標的平滑性和應答性
  3. 壓力測試: 同時連接多個無人機並監控 CPU 使用率
  4. QDebug: 在時間敏感的部分添加 print() 以測量執行時間

更新日期: 2025-01-20
版本: 執行緒安全優化 v1.0