一、問題的來源與分析

     首先,我們要知道 “為什么Qt Gui 會停止響應(yīng)?”。簡明扼要的說就是:長時間的密集處理或等待阻塞了Qt的事件循環(huán),應(yīng)用程序不能響應(yīng)來自窗口系統(tǒng)的事件請求(《C++ Gui Qt4》 P135中有描述)。   那么多長算長呢?一秒鐘算長,兩秒鐘太長。

     其次,“ 何種情形下會發(fā)生該問題? ”??煞譃閮煞N情形:

     第一,長時間按順序執(zhí)行的密集運(yùn)算,全部計(jì)算結(jié)束后才能繼續(xù)執(zhí)行,如快速傅立葉變換。

     第二,“ 觸發(fā) ”了某項(xiàng)操作,該操作完成后才能進(jìn)行“ 下一步 ”, 所以這里描述的是異步操作,如保存文件操作,服務(wù)器等待連接、網(wǎng)絡(luò)下載等。詳細(xì)見附注(1).

     私以為兩種情形并無明顯的概念上的區(qū)分,本質(zhì)是一樣的,但兩種情形有不同的處理方法,特別是第二種情形, 在Qt框架下 ,用Qt的信號和槽機(jī)制往往可以解決阻塞問題,如QTcpServer::newConnection信號通知連接的到來,QIODevice::bytesWritten()與 QIODevice::readyRead()通知文件的讀寫,它們都是以非阻塞的形式實(shí)現(xiàn)相關(guān)功能的利器。 而第一種情形,不僅所有的事件循環(huán)停止了,信號和槽也暫時被忽視。我們將針對以上兩種情形尋找解決方案。

     最后,我們考慮是否可以把這個造成 Qt Gui 停止響應(yīng)的罪魁禍?zhǔn)状笮栋藟K,即把他拆分成一個個小塊,如果可以拆分,那么每塊之間是依賴還是獨(dú)立,如果獨(dú)立那問題好辦,放在不同的位置獨(dú)立運(yùn)作,否則,我們只能同步的執(zhí)行,而最差的結(jié)果是——根本無法拆分??!

     總之,考慮以上信息差異,執(zhí)行不同的解決方案。


二、解決方案

Manual event processing(人工執(zhí)行事件)

     保持事件循環(huán)有一種最基本的方法——讓程序去處理 懸掛事件好了 ,處理完了再回來繼續(xù)我的后續(xù)運(yùn)算,要做到這一點(diǎn),就要在我的運(yùn)算代碼中間加上處理事件的代碼,這句代碼就是 QCoreApplication::processEvents();,只要該句代碼能夠周期性的被執(zhí)行,就能保持Qt Gui的響應(yīng)。

//代碼來源于上述鏈接所指向文章

復(fù)制代碼
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;
        } 

    }
復(fù)制代碼

部分翻譯(略)——可查看 Jason Lee的翻譯

     該方案除了 具有Witold Wysota文中 所提到缺點(diǎn)之外,《C++ Gui Qt4》P135中還提到,用戶可能會在應(yīng)用程序還在執(zhí)行某種操作時,或者關(guān)閉了主窗口,或者通過界面再次觸發(fā)相同操作,這樣就會產(chǎn)生不可預(yù)料的后果,如 一個保存文件對話框,用戶單擊save按鈕后,程序開始磁盤文件的寫入操作,該操作還未完成時,用戶再次單擊了關(guān)閉按鈕,或者再次單擊save按鈕。書中給出的解決辦法是將 qApp->processEvents()替換為qApp-> processEvents(QEventLoop::ExcludeUserInputEvents),以告訴Qt忽略鼠標(biāo)事件和鍵盤事件。

 

Using a Worker Thread(使用任務(wù)線程)

     翻譯(略)—— Jason Lee的翻譯

除了 Witold Wysota 文中所說的重新實(shí)現(xiàn)QThread類之外,還可以使用QObject::moveToThread(QThread *thread)函數(shù),將

進(jìn)行復(fù)雜運(yùn)算的對象移入子線程中運(yùn)行,前提是子線程不能夠有父對象,否則無法移入子線程。示例如下:

QThread *thread = new QThread(this); 
MyComputation *computation = new MyComputation();//負(fù)責(zé)密集運(yùn)算的對象  computation->moveToThread(thread); 
connect(thread, SIGNAL(started()), computation ,SLOT( compute()) ); //compute()為computation的運(yùn)算函數(shù)  thread->start();

需注意的是,將 computation 對象移入子線程后, 依舊不可直接調(diào)用  computation 對象 compute()函,應(yīng)該調(diào)用線程對象的start()函數(shù),發(fā)出started()信號觸發(fā) computation 對象的運(yùn)算操作,否則依舊會阻塞主線程。

 

Waiting in a Local Event Loop(在本地事件循環(huán)中等待)

翻譯(略)—— Jason Lee的翻譯

注解:如文章開頭所說,“等待異步事件完成”,也就是說這種方法是針對異步事件而設(shè)計(jì)的,異步事件執(zhí)行過程中會不斷發(fā)送信號,我們根據(jù)該信號決定程序接下來的行為,包括人工執(zhí)行事件。而  Manual event processing 適用于順序執(zhí)行的操作 。

 

Solving a Problem Step by Step(分步驟解決問題)

翻譯(略)

如前文所說,如果一個復(fù)雜操作可以拆分為獨(dú)立的子操作,那么拆分應(yīng)該是最好的解決辦法。至于如何拆分,可以通過閱讀《重構(gòu)》這本書來學(xué)習(xí)。

 

Parallel Programming 
翻譯(略)


三、總結(jié)

     前面我提到過,我是用queryBatch()函數(shù)導(dǎo)致了Qt Gui無法響應(yīng)的,最后我選擇了 Using a Worker Thread這種方法。queryBatch()是一個操作數(shù)據(jù)庫的批處理函數(shù),非常便利,但它是順序執(zhí)行的,我無法用異步方式來處理它,函數(shù)內(nèi)部是不可見的,也無法人工執(zhí)行事件或者拆分,最后只能使用子線程來執(zhí)行它了,這就是為便利所付出的代價吧。不同情況有不同的解決方案,認(rèn)清自己的問題很重要。


四、附注

(1) “ 觸發(fā) ”了某項(xiàng)操作,該操作完成后才能進(jìn)行“ 下一步 ”,但實(shí)際上對于“觸發(fā)”這個行為本身而言,它的職責(zé)已經(jīng)完成了,而

“下一步”指的是某個功能執(zhí)行中的“下一步”,當(dāng)然也可能是編程語境下的“ 下一步 ”,即下一條語句, 所以這里描述的是異步操作,如保存文件操作,服務(wù)器等待連接、網(wǎng)絡(luò)下載等。

     舉個栗子, 創(chuàng)建了QTcpServer對象并調(diào)用listen() 函數(shù)監(jiān)聽連接,這時如果調(diào)用QTcpServer::waitForNewConnection(...)函數(shù),就會阻塞程序的運(yùn)行,直到連接到來函數(shù)才能返回,進(jìn)而執(zhí)行下一句。這里,listen()函數(shù)“ 觸發(fā) ”了監(jiān)聽行為, “ 下一步 ”與網(wǎng)絡(luò)連接相關(guān)的動作要等連接到來后才能執(zhí)行,而 QTcpServer::waitForNewConnection(...)則作為我們是否執(zhí)行下一步的判斷標(biāo)準(zhǔn),只不過這里用的是阻塞的方式;對于異步操作,也可以用非阻塞的方式解決,Qt的信號和槽機(jī)制就能很好的解決,如我們可以接收newConnection()信號判斷連接是否到來,而不必將程序阻塞在那。