最開始使用Qt時就遇到過QT Gui失去響應的問題,我是用多線程的方式解決的,然而一般來講,多線程是會下降程序的運行速度。html
以後,在使用QSqlQuery::execBatch()函數時,Qt Gui 又失去響應,雖然多線程能夠解決,可是若是能用單線程很好解決的,最好不要用到多線程,由於多線程不只容易拖慢程序的速度,編程及維護的難度也更大,能用簡單方法解決的,就不要用複雜的方法。sql
因而我再次搜索資料,指望在解決方案的選擇與解決步驟上,可以獲得一個全面而又細緻的總結。數據庫
Witold Wysota 的文章https://doc.qt.io/archives/qq/qq27-responsive-guis.html#performinglongoperations 總結的很是不錯。編程
Jason Lee的翻譯: http://blog.csdn.net/jasonblog/article/details/5568589服務器
因此本文是在此文基礎上的部分翻譯、理解與二次總結。總之,有刪減,有補充,因此沒寫 '轉' 字。網絡
1、問題的來源與分析多線程
首先,咱們要知道 「爲何Qt Gui 會中止響應?」。簡明扼要的說就是:長時間的密集處理或等待阻塞了Qt的事件循環,應用程序不能響應來自窗口系統的事件請求(《C++ Gui Qt4》 P135中有描述)。 那麼多長算長呢?一秒鐘算長,兩秒鐘太長。框架
其次,「 何種情形下會發生該問題? 」。可分爲兩種情形:異步
第一,長時間按順序執行的密集運算,所有計算結束後才能繼續執行,如快速傅立葉變換。函數
第二,「 觸發 」了某項操做,該操做完成後才能進行「 下一步 」, 因此這裏描述的是異步操做,如保存文件操做,服務器等待鏈接、網絡下載等。詳細見附註(1).
私覺得兩種情形並沒有明顯的概念上的區分,本質是同樣的,但兩種情形有不一樣的處理方法,特別是第二種情形, 在Qt框架下 ,用Qt的信號和槽機制每每能夠解決阻塞問題,如QTcpServer::newConnection信號通知鏈接的到來,QIODevice::bytesWritten()與 QIODevice::readyRead()通知文件的讀寫,它們都是以非阻塞的形式實現相關功能的利器。 而第一種情形,不只全部的事件循環中止了,信號和槽也暫時被忽視。咱們將針對以上兩種情形尋找解決方案。
最後,咱們考慮是否能夠把這個形成 Qt Gui 中止響應的罪魁禍首大卸八塊,即把他拆分紅一個個小塊,若是能夠拆分,那麼每塊之間是依賴仍是獨立,若是獨立那問題好辦,放在不一樣的位置獨立運做,不然,咱們只能同步的執行,而最差的結果是——根本沒法拆分!!
總之,考慮以上信息差別,執行不一樣的解決方案。
2、解決方案
Manual event processing(人工執行事件)
保持事件循環有一種最基本的方法——讓程序去處理 懸掛事件好了 ,處理完了再回來繼續個人後續運算,要作到這一點,就要在個人運算代碼中間加上處理事件的代碼,這句代碼就是 QCoreApplication::processEvents();,只要該句代碼可以週期性的被執行,就能保持Qt Gui的響應。
//代碼來源於上述連接所指向文章
for (int i = 3; i <= sqrt(x) && isPrime; i += 2) { label->setText(tr("Checking %1...").arg(i)); if (x % i == 0) isPrime = false; QCoreApplication::processEvents(); if (!pushButton->isChecked()) { label->setText(tr("Aborted")); return; } }
部分翻譯(略)——可查看 Jason Lee的翻譯
該方案除了 具備Witold Wysota文中 所提到缺點以外,《C++ Gui Qt4》P135中還提到,用戶可能會在應用程序還在執行某種操做時,或者關閉了主窗口,或者經過界面再次觸發相同操做,這樣就會產生不可預料的後果,如 一個保存文件對話框,用戶單擊save按鈕後,程序開始磁盤文件的寫入操做,該操做還未完成時,用戶再次單擊了關閉按鈕,或者再次單擊save按鈕。書中給出的解決辦法是將 qApp->processEvents()替換爲qApp-> processEvents(QEventLoop::ExcludeUserInputEvents),以告訴Qt忽略鼠標事件和鍵盤事件。
Using a Worker Thread(使用任務線程)
翻譯(略)—— Jason Lee的翻譯
除了 Witold Wysota 文中所說的從新實現QThread類以外,還可使用QObject::moveToThread(QThread *thread)函數,將
進行復雜運算的對象移入子線程中運行,前提是子線程不可以有父對象,不然沒法移入子線程。示例以下:
QThread *thread = new QThread(this); MyComputation *computation = new MyComputation();//負責密集運算的對象 computation->moveToThread(thread); connect(thread, SIGNAL(started()), computation ,SLOT( compute()) ); //compute()爲computation的運算函數 thread->start();
需注意的是,將 computation 對象移入子線程後, 依舊不可直接調用 computation 對象 compute()函,應該調用線程對象的start()函數,發出started()信號觸發 computation 對象的運算操做,不然依舊會阻塞主線程。
Waiting in a Local Event Loop(在本地事件循環中等待)
翻譯(略)—— Jason Lee的翻譯
註解:如文章開頭所說,「等待異步事件完成」,也就是說這種方法是針對異步事件而設計的,異步事件執行過程當中會不斷髮送信號,咱們根據該信號決定程序接下來的行爲,包括人工執行事件。而 Manual event processing 適用於順序執行的操做 。
Solving a Problem Step by Step(分步驟解決問題)
翻譯(略)
如前文所說,若是一個複雜操做能夠拆分爲獨立的子操做,那麼拆分應該是最好的解決辦法。至於如何拆分,能夠經過閱讀《重構》這本書來學習。
Parallel Programming
翻譯(略)
3、總結
前面我提到過,我是用queryBatch()函數致使了Qt Gui沒法響應的,最後我選擇了 Using a Worker Thread這種方法。queryBatch()是一個操做數據庫的批處理函數,很是便利,但它是順序執行的,我沒法用異步方式來處理它,函數內部是不可見的,也沒法人工執行事件或者拆分,最後只能使用子線程來執行它了,這就是爲便利所付出的代價吧。不一樣狀況有不一樣的解決方案,認清本身的問題很重要。
4、附註
(1) 「 觸發 」了某項操做,該操做完成後才能進行「 下一步 」,但實際上對於「觸發」這個行爲自己而言,它的職責已經完成了,而
「下一步」指的是某個功能執行中的「下一步」,固然也多是編程語境下的「 下一步 」,即下一條語句, 因此這裏描述的是異步操做,如保存文件操做,服務器等待鏈接、網絡下載等。
舉個栗子, 建立了QTcpServer對象並調用listen() 函數監聽鏈接,這時若是調用QTcpServer::waitForNewConnection(...)函數,就會阻塞程序的運行,直到鏈接到來函數才能返回,進而執行下一句。這裏,listen()函數「 觸發 」了監聽行爲, 「 下一步 」與網絡鏈接相關的動做要等鏈接到來後才能執行,而 QTcpServer::waitForNewConnection(...)則做爲咱們是否執行下一步的判斷標準,只不過這裏用的是阻塞的方式;對於異步操做,也能夠用非阻塞的方式解決,Qt的信號和槽機制就能很好的解決,如咱們能夠接收newConnection()信號判斷鏈接是否到來,而沒必要將程序阻塞在那。