Qt 多線程設計:死循環與信號槽的權衡

在開發音視頻播放器時,多線程設計是不可避免的挑戰。音頻和視頻的解碼、播放需要高效運行,同時還要與主線程或其他線程同步,例如通過信號通知播放進度。本文基于一個實際案例,分析了兩種線程設計在死循環和信號槽使用中的表現,探討其原因,并給出選擇建議。

問題表現

我在實現音頻播放線程時,遇到了一個問題:主線程通過 QMetaObject::invokeMethod 調用 terminateDecode 無法終止音頻線程,而直接調用 m_audioThread->terminateDecode() 卻能穩定生效。后來,我將視頻解碼線程改為 QObject + QThread + std::thread 的設計后,invokeMethod 開始生效。這讓我疑惑:為什么死循環會影響信號槽?如何選擇合適的設計?

具體表現如下:

  1. 音頻線程(AudioPlayerThread)
    • 使用 QThread 子類,重寫 run() 為死循環。
    • 主線程調用 QMetaObject::invokeMethod(m_audioThread, "terminateDecode", Qt::QueuedConnection) 無效。
    • 直接調用 m_audioThread->terminateDecode() 生效。
  2. 視頻線程(m_videoWorker)
    • 使用 QObject 移入 QThread,解碼死循環在 std::thread 中。
    • QMetaObject::invokeMethod(m_videoWorker, "terminateDecode", Qt::QueuedConnection) 生效。

此外,音頻線程需要發送 avClockUpdated 信號與視頻同步,這在死循環中似乎不受影響。問題出在哪里?

原因分析

問題的根源在于 Qt 的信號槽機制與線程設計的關系,特別是事件循環的作用。

1. 信號發送(emit)不受死循環影響

  • 機制:在 Qt 中,emit 一個信號會根據連接類型(Qt::DirectConnection 或 Qt::QueuedConnection)直接調用槽函數或將信號放入目標線程的事件隊列。emit 本身是線程安全的,不依賴事件循環。
  • 音頻線程的表現
    emit avClockUpdated(current_pts);

  • 在 AudioPlayerThread 的死循環中,只要線程執行到 emit,信號就會發出,通知視頻線程或其他組件。死循環不會阻止信號發送。

2. 信號接收需要事件循環

  • 機制:當使用 Qt::QueuedConnection 調用槽函數(如 terminateDecode),Qt 會將調用請求放入目標線程的事件隊列,等待事件循環處理。如果線程沒有事件循環,隊列中的信號無法被執行。
  • 音頻線程的問題
    • run() 是一個 while (true) 死循環:

void AudioPlayerThread::run() {while (true) {// 等待文件和播放邏輯}
}

?

    • 沒有調用 exec(),事件循環未啟動。
    • QMetaObject::invokeMethod 的調用被排隊但無法處理,因此無效。
  • 直接調用的原因
    • m_audioThread->terminateDecode() 是同步調用,不依賴事件循環,直接修改 m_stopRequested 并喚醒條件變量,線程得以退出。

3. 視頻線程的改進

  • 設計

    • m_videoWorker 是 QObject,通過 moveToThread 移入 QThread。
    • QThread 默認運行事件循環(exec())。
    • 解碼死循環在 std::thread 中:
      class VideoWorker : public QObject {
      public:VideoWorker() {m_decodeThread = std::thread([this] { decodeLoop(); });}
      public slots:void terminateDecode() { m_stopRequested = true; }
      private:void decodeLoop() { while (!m_stopRequested) { /* 解碼 */ } }std::thread m_decodeThread;std::atomic<bool> m_stopRequested{false};
      };



      ?

    • 結果
      • QThread 的事件循環處理 invokeMethod,觸發 terminateDecode。
      • 死循環在 std::thread 中,不干擾事件循環。
    • 4. 音視頻同步的需求

    • 音頻線程發出 avClockUpdated 信號,視頻線程接收以同步。
    • 如果視頻需要控制音頻(例如暫停),音頻線程也需要接收信號。死循環設計在這方面受限。
    • 設計選擇

      基于以上分析,我對比了兩種設計:

      設計 1:AudioPlayerThread(QThread 死循環)

    • 特點
      • 重寫 run() 為死循環,使用條件變量同步。
      • 通過直接調用控制線程。
    • 優點
      • 高效:無事件循環開銷,適合音頻實時性。
      • 簡單:邏輯集中,易于實現。
    • 缺點
      • 無法接收信號:需手動檢查狀態。
      • 擴展性差:復雜控制需額外同步。
    • 適用場景:單一任務,實時性要求高。
    • 設計 2:m_videoWorker(QObject + QThread + std::thread)

    • 特點
      • QThread 運行事件循環,std::thread 執行死循環。
      • 通過信號槽控制。
    • 如何選擇?

    • 音頻實時性優先:選擇設計 1,優化終止邏輯(例如在內層檢查 m_stopRequested)。
    • 音視頻同步和控制:選擇設計 2,支持雙向信號通信。
    • 我的需求:音頻需要發送 avClockUpdated 與視頻同步,可能還需要接收控制信號。設計 2 更合適。
    • 優化建議

      對于音視頻同步,我推薦設計 2:

    • 優點
      • 支持信號槽:發送和接收信號都方便。
      • 擴展性強:適合音視頻同步和復雜交互。
    • 缺點
      • 復雜性增加:多線程管理。
      • 輕微開銷:事件循環和線程切換。
    • 適用場景:需要雙向通信或 UI 交互。

?

// 音頻工作類
class AudioWorker : public QObject {Q_OBJECT
public:AudioWorker() { m_audioThread = std::thread([this] { audioLoop(); }); }~AudioWorker() { m_stopRequested = true; if (m_audioThread.joinable()) m_audioThread.join(); }
public slots:void terminateDecode() { m_stopRequested = true; }
signals:void avClockUpdated(qint64 pts); // 發送音頻時間戳
private:void audioLoop() {while (!m_stopRequested) {qint64 pts = /* 計算音頻時間戳,例如 av_rescale_q */;emit avClockUpdated(pts); // 通知視頻線程// 音頻解碼和播放邏輯std::this_thread::sleep_for(std::chrono::milliseconds(10)); // 模擬播放}}std::thread m_audioThread;std::atomic<bool> m_stopRequested{false};
};// 視頻工作類
class VideoWorker : public QObject {Q_OBJECT
public:VideoWorker() { m_videoThread = std::thread([this] { videoLoop(); }); }~VideoWorker() { m_stopRequested = true; if (m_videoThread.joinable()) m_videoThread.join(); }
public slots:void terminateDecode() { m_stopRequested = true; }void syncWithAudio(qint64 audioPts) { m_currentAudioPts = audioPts; // 根據音頻時間戳調整視頻播放}
private:void videoLoop() {while (!m_stopRequested) {qint64 videoPts = /* 計算視頻時間戳 */;if (m_currentAudioPts > 0 && std::abs(videoPts - m_currentAudioPts) > 100) {// 如果視頻與音頻時間差過大,調整播放(例如跳幀或等待)}// 視頻解碼和渲染邏輯std::this_thread::sleep_for(std::chrono::milliseconds(40)); // 模擬播放}}std::thread m_videoThread;std::atomic<bool> m_stopRequested{false};std::atomic<qint64> m_currentAudioPts{0};
};// 主線程中的管理類
class MainClass : public QObject {Q_OBJECT
public:void startAV() {// 初始化音頻線程m_audioThread = new QThread();m_audioWorker = new AudioWorker();m_audioWorker->moveToThread(m_audioThread);// 初始化視頻線程m_videoThread = new QThread();m_videoWorker = new VideoWorker();m_videoWorker->moveToThread(m_videoThread);// 連接音頻信號到視頻槽,實現同步connect(m_audioWorker, &AudioWorker::avClockUpdated, m_videoWorker, &VideoWorker::syncWithAudio, Qt::QueuedConnection);// 啟動線程m_audioThread->start();m_videoThread->start();}void stopAV() {if (m_audioThread) {QMetaObject::invokeMethod(m_audioWorker, "terminateDecode", Qt::QueuedConnection);m_audioThread->quit();m_audioThread->wait();delete m_audioThread;delete m_audioWorker;}if (m_videoThread) {QMetaObject::invokeMethod(m_videoWorker, "terminateDecode", Qt::QueuedConnection);m_videoThread->quit();m_videoThread->wait();delete m_videoThread;delete m_videoWorker;}}private:QThread* m_audioThread = nullptr;AudioWorker* m_audioWorker = nullptr;QThread* m_videoThread = nullptr;VideoWorker* m_videoWorker = nullptr;
};

?

總結

  • 信號發送:死循環不影響 emit,音頻可以通知視頻。
  • 信號接收:死循環無事件循環,需用設計 2 或狀態檢查解決。
  • 選擇依據:實時性選設計 1,同步和擴展性選設計 2。

通過將死循環移到 std::thread,結合 QThread 的事件循環,我實現了音頻和視頻的高效同步。這種設計既滿足了實時性,又提供了靈活性,是音視頻播放器的推薦方案。

?

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/73968.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/73968.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/73968.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

knowledge-微前端(多個前端應用聚合的一個應用架構體系,每個小的應用可獨立運行,獨立開發,獨立部署上線)

1.前言 微前端&#xff0c;將一個大的前端應用拆分為多個小型的&#xff0c;獨立開發的前端應用&#xff0c;每一個小型的應用都可以單獨的開發&#xff0c;部署和運行。這種結構允許不同的團隊使用不同的技術棧來開發應用的不同部分&#xff0c;提高開發的效率與靈活性。 2.實…

工廠函數詳解:概念、目的與作用

一、什么是工廠函數&#xff1f; 工廠函數&#xff08;Factory Function&#xff09;是一種設計模式&#xff0c;其核心是通過一個函數來 創建并返回對象&#xff0c;而不是直接使用 new 或構造函數實例化對象。它封裝了對象的創建過程&#xff0c;使代碼更靈活、可維護。 二、…

旋轉位置編碼(Rotary Positional Encoding, RoPE):中文公式詳解與代碼實現

旋轉位置編碼&#xff08;Rotary Positional Encoding, RoPE&#xff09;&#xff1a;中文公式詳解與代碼實現 在序列模型中&#xff0c;位置信息對于任務的理解至關重要。傳統的絕對和相對位置編碼各有優缺點&#xff0c;而RoPE作為一種創新的位置編碼方法&#xff0c;展現了…

C語言-指針變量和變量指針

指針 預備知識 內存地址 字節&#xff1a;字節是內存的容量單位&#xff0c;英文名Byte&#xff0c;1Byte8bits 地址&#xff1a;系統為了便于區分每一個字節面對它們的逐一進行編號&#xff08;編號是唯一的&#xff09;&#xff0c;稱為內存地址&#xff0c;簡稱地址。int…

unityAB包(1/2)

unityAB包學習 1.AB包的導出擴展BuildAssetBundleOptions無特殊選項壓縮相關選項 2.AB包資源管理3.Resource和AssetBundle加載方式的區別4.預設體5.Unity Asset Bundle Browser 工具5為什么要勾選拷貝到StreamingAsset里面。6.AB包的加載 1.AB包的導出 首先在Project窗口&…

算法——廣度優先搜索——跨步迷宮

原題鏈接 思路&#xff1a;找出最短路徑&#xff0c;然后判斷是否存在連續三個點是橫縱坐標相等的&#xff0c;如果有就步數減1 但是有兩個樣例過不了 錯誤原因&#xff1a;在錯誤的測試案例中&#xff0c;最短路徑可能有多條&#xff0c;而我剛好選了一條比較曲折的&#x…

某酒企數字化轉型及電商規劃項目啟動會暨培訓會v(60頁PPT)(文末有下載方式)

詳細資料請看本解讀文章的最后內容。 在當今數字化浪潮席卷之下&#xff0c;企業的發展面臨著前所未有的機遇與挑戰。對于某酒企而言&#xff0c;數字化轉型和電商規劃已成為其實現 “二次騰飛”、邁向世界級酒企的關鍵戰略舉措。本次啟動會暨培訓會&#xff0c;為該酒企的轉型…

NET6 WebApi第5講:中間件(源碼理解,俄羅斯套娃怎么來的?);Web 服務器 (Nginx / IIS / Kestrel)、WSL、SSL/TSL

一、NET6的啟動流程 區別&#xff1a; .NET6 WebApi第1講&#xff1a;VSCode開發.NET項目、區別.NET5框架【兩個框架啟動流程詳解】_vscode webapi-CSDN博客 2、WebApplicationBuilder&#xff1a;是NET6引入的一個類&#xff0c;是建造者模式的典型應用 1>建造者模式的…

vue中根據html動態渲染內容

需求&#xff1a;根據數據中的html&#xff0c;因為我是在做填空&#xff0c;所以是需要將html中的_____替換成input&#xff0c;由于具體需求我使用的是元素contenteditable代替的可編輯的input html部分 <div class"wrap"><component :is"rendered…

【AI】AI編程助手:Cursor、Codeium、GitHub Copilot、Roo Cline、Tabnine

文章目錄 一、基本特性對比二、收費標準三、私有部署能力1、Tabnine2、Roo Code 三、代碼補全與自然語言生成代碼四、安裝獨立的IDE安裝插件安裝 五、基本使用&#xff08;一&#xff09;Cursor&#xff08;二&#xff09;GitHub Copilot1、獲取代碼建議2.聊天1&#xff09;上下…

三軸云臺之角速度信號篇

三軸云臺的角速度信號主要通過其內置的傳感器&#xff08;如陀螺儀&#xff09;來感知和測量。 一、角速度信號的感知與測量 在三軸云臺中&#xff0c;陀螺儀是測量角速度的關鍵組件。它通常安裝在三個互相垂直的軸上&#xff08;通常為X、Y、Z軸&#xff09;&#xff0c;能夠…

Grid 布局實現三欄布局

使用 CSS Grid 布局實現三欄布局(左右固定 100px,中間自適應)的核心原理是通過網格模板精確控制列寬分配。以下是具體實現方法及優化技巧: 一、基礎實現 ?父容器設置 為外層容器添加 display: grid 使其成為網格容器,并通過 grid-template-columns 定義列寬 css .contain…

綠盟春招實習一面

《網安面試指南》https://mp.weixin.qq.com/s/RIVYDmxI9g_TgGrpbdDKtA?token1860256701&langzh_CN 5000篇網安資料庫https://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247486065&idx2&snb30ade8200e842743339d428f414475e&chksmc0e4732df793fa3bf39…

進制轉換(R轉十)(1290. 二進制轉換十進制、1292. 十六進制轉十進制、1291. 八進制轉十進制、1405. 小麗找潛在的素數)

題單地址&#xff1a;題單中心-東方博宜OJ 這里以二進制轉十進制為例&#xff08;按位加權求和法&#xff09; 1290. 二進制轉換十進制 問題描述 請將一個 25 位以內的 2 進制正整數轉換為 1010 進制&#xff01; 輸入 一個 25 位以內的二進制正整數。 輸出 該數對應的…

Redis 本地安裝

首先安裝&#xff1a; https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-from-source/ 進入root目錄 tar -xzvf redis-stable.tar.gz cd redis-stable make然后 install sudo make install最后可以直接啟動 redis-server但是此時啟…

9.嗅探與Wireshark進階分析

嗅探與Wireshark進階分析 第一部分&#xff1a;嗅探的概念與重要性第二部分&#xff1a;Wireshark進階功能第三部分&#xff1a;嗅探實踐與分析總結 目標&#xff1a; ? 理解嗅探&#xff08;Sniffing&#xff09;的概念及其在網絡安全中的作用 ? 掌握Wireshark的進階功能&a…

在 VSCode 遠程開發環境下使用 Git 常用命令

在日常開發過程中&#xff0c;無論是單人項目還是團隊協作&#xff0c;Git 都是版本管理的利器。尤其是在使用 VSCode 連接遠程服務器進行代碼開發時&#xff0c;Git 不僅能幫助你管理代碼版本&#xff0c;還能讓多人協作變得更加高效。本文將介紹一些常用的 Git 命令&#xff…

npm 命令使用文檔

目錄 簡介安裝與配置基礎命令依賴管理版本控制腳本管理包發布高級命令配置管理最佳實踐常見問題 1. 簡介 npm (Node Package Manager) 是 Node.js 的官方包管理工具&#xff0c;提供&#xff1a; 130萬 開源包的注冊表訪問依賴解析與版本管理項目腳本自動化私有包管理能力完…

【Linux篇】進程控制

&#x1f4cc; 個人主頁&#xff1a; 孫同學_ &#x1f527; 文章專欄&#xff1a;Liunx &#x1f4a1; 關注我&#xff0c;分享經驗&#xff0c;助你少走彎路&#xff01; 1. 進程創建 1.1 fork函數 在linux中fork函數是非常重要的函數&#xff0c;它從已存在進程中創建一個…

HyperAD:學習弱監督音視頻暴力檢測在雙曲空間中的方法

文章目錄 速覽摘要1. 引言2. 相關工作弱監督暴力檢測雙曲空間中的神經網絡 3. 預備知識雙曲幾何切空間&#xff08;Tangent Space&#xff09;指數映射與對數映射&#xff08;Exponential and Logarithmic Maps&#xff09;3.1 雙曲圖卷積網絡&#xff08;Hyperbolic Graph Con…