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.
AirTrapMine/src/GUI/THREAD_SAFETY.md

219 lines
7.0 KiB
Markdown

1 month ago
# 執行緒安全性實現 - 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 (布林鎖)**
```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