Qt入門(9)——Qt中的執行緒支援

尹成發表於2014-09-30

Qt對執行緒提供了支援,基本形式有獨立於平臺的執行緒類、執行緒安全方式的事件傳遞和一個全域性Qt庫互斥量允許你可以從不同的執行緒呼叫Qt方法。

警告:所有的GUI類(比如,QWidget和它的子類),作業系統核心類(比如,QProcess)和網路類都不是執行緒安全的。

QRegExp使用一個靜態快取並且也不是執行緒安全的,即使通過使用QMutex來保護的QRegExp物件。


啟用執行緒支援
在Windows上安裝Qt時,在一些編譯器上執行緒支援是一個選項。
在Mac OS X和Unix上,執行緒支援可以當你在執行configure指令碼時新增-thread選項就可以生效了。在Unix平臺上,多執行緒程式必須用特殊的方式連線,比如使用特殊的libc,安裝程式將會建立另外一個庫libqt-mt並且因此執行緒程式必須和這個庫進行連線(使用-lqt-mt)而不是標準的Qt庫。
在兩個平臺上,你都應該定義巨集QT_THREAD_SUPPORT來編譯(比如,編譯時使用-DQT_THREAD_SUPPORT)。在Windows上,這個通常可以在qconfig.h寫一個條目來解決。

執行緒類
最重要的類是QThread,也就是說要開始一個新的執行緒,就是開始執行你重新實現的QThread::run()。這和Java的執行緒類很相似。
為了寫執行緒程式,在兩個執行緒同時希望訪問同一個資料時,對資料進行保護是很必要的。因此這裡也有一個QMutex類,一個執行緒可以鎖定互斥量,並且在它鎖定之後,其它執行緒就不能再鎖定這個互斥量了,試圖這樣做的執行緒都會被阻塞直到互斥量被釋放。例如:

    class MyClass
    {
    public:
        void doStuff( int );


    private:
        QMutex mutex;
        int a;
        int b;
    };


    // 這裡設定a為c,b為c*2。


    void MyClass::doStuff( int c )
    {
        mutex.lock();
        a = c;
        b = c * 2;
        mutex.unlock();
    } 


這保證了同一時間只有一個執行緒可以進入MyClass::doStuff(),所以b將永遠等於c * 2。


另外一個執行緒也需要在一個給定的條件下等待其它執行緒的喚醒,QWaitCondition類就被提供了。執行緒等待的條件QWaitCondition指出發生了什麼事情,阻塞將一直持續到這種事情發生。當某種事情發生了,QWaitCondition可以喚醒等待這一事件的執行緒之一或全部。(這和POSIX執行緒條件變數是具有相同功能的並且它也是Unix上的一種實現。)例如:

    #include <qapplication.h>
    #include <qpushbutton.h>


    // 全域性條件變數
    QWaitCondition mycond;


    // Worker類實現
    class Worker : public QPushButton, public QThread
    {
        Q_OBJECT


    public:
        Worker(QWidget *parent = 0, const char *name = 0)
            : QPushButton(parent, name)
        {
            setText("Start Working");


            // 連線從QPushButton繼承來的訊號和我們的slotClicked()方法
            connect(this, SIGNAL(clicked()), SLOT(slotClicked()));


            // 呼叫從QThread繼承來的start()方法……這將立即開始執行緒的執行
            QThread::start();
        }


    public slots:
        void slotClicked()
        {
            // 喚醒等待這個條件變數的一個執行緒
            mycond.wakeOne();
        }


    protected:
        void run()
        {
            // 這個方法將被新建立的執行緒呼叫……


            while ( TRUE ) {
                // 鎖定應用程式互斥鎖,並且設定視窗標題來表明我們正在等待開始工作
                qApp->lock();
                setCaption( "Waiting" );
                qApp->unlock();


                // 等待直到我們被告知可以繼續
                mycond.wait();


                // 如果我們到了這裡,我們已經被另一個執行緒喚醒……讓我們來設定標題來表明我們正在工作
                qApp->lock();
                setCaption( "Working!" );
                qApp->unlock();


                // 這可能會佔用一些時間,幾秒、幾分鐘或者幾小時等等,因為這個一個和GUI執行緒分開的執行緒,在處理事件時,GUI執行緒不會停下來……
                do_complicated_thing();
            }
        }
    };


	// 主執行緒——所有的GUI事件都由這個執行緒處理。
    int main( int argc, char **argv )
    {
        QApplication app( argc, argv );


        // 建立一個worker……當我們這樣做的時候,這個worker將在一個執行緒中執行
        Worker firstworker( 0, "worker" );


        app.setMainWidget( &worker );
        worker.show();


        return app.exec();
    }


  
只要你按下按鈕,這個程式就會喚醒worker執行緒,這個執行緒將會進行並且做一些工作並且然後會回來繼續等待被告知做更多的工作。如果當按鈕被按下時,worker執行緒正在工作,那麼就什麼也不會發生。當執行緒完成了工作並且再次呼叫QWaitCondition::wait(),然後它就會被開始。


執行緒安全的事件傳遞
在Qt中,一個執行緒總是一個事件執行緒——確實是這樣的,執行緒從視窗系統中拉出事件並且把它們分發給視窗部件。靜態方法QThread::postEvent從執行緒中傳遞事件,而不同於事件執行緒。事件執行緒被喚醒並且事件就像一個普通視窗系統事件那樣在事件執行緒中被分發。例如,你可以強制一個視窗部件通過如下這樣做的一個不同的執行緒來進行重繪:


    QWidget *mywidget;
    QThread::postEvent( mywidget, new QPaintEvent( QRect(0, 0, 100, 100) ) );


  
這(非同步地)將使mywidget重繪一塊100*100的正方形區域。


Qt庫互斥量
Qt庫互斥量提供了從執行緒而不是事件執行緒中呼叫Qt方法的一種方法。例如:

  QApplication *qApp;
  QWidget *mywidget;


  qApp->lock();


  mywidget->setGeometry(0,0,100,100);


  QPainter p;
  p.begin(mywidget);
  p.drawLine(0,0,100,100);
  p.end();


  qApp->unlock();


  
在Qt中沒有使用互斥量而呼叫一個函式通常情況下結果將是不可預知的。從另外一個執行緒中呼叫Qt的一個GUI相關函式需要使用Qt庫互斥量。在這種情況下,所有可能最終訪問任何圖形或者視窗系統資源的都是GUI相關的。使用容器類,字串或者輸入/輸出類,如果物件只被一個執行緒使用就不需要任何互斥量了。


當進行執行緒程式設計時,需要注意的一些事情:


當使用Qt庫互斥量的時候不要做任何阻塞操作。這將會凍結事件迴圈。
確認你鎖定一個遞迴QMutex的次數和解鎖的次數一樣,不能多也不能少。
在呼叫除了Qt容器和工具類的任何東西之前鎖定Qt應用程式互斥量。
謹防隱含地共享類,你應該避免線上程之間使用操作符=()來複制它們。這將會在Qt的未來主要的或次要的發行版本中進行改進。
謹防那些沒有被設計為執行緒安全的Qt類,例如,QPtrList的應用程式介面就不是執行緒安全的並且如果不同的執行緒需要遍歷一個QPtrList,它們應該在呼叫QPtrList::first()之前鎖定並且在到達終點之後解鎖,而不是在QPtrList::next()的前後進行鎖定和解鎖。
確認只在GUI執行緒中建立的繼承和使用了QWidget、QTimer和QSocketNotifier的物件。在一些平臺上,在某個不是GUI執行緒的執行緒中建立這樣的物件將永遠不會接受到底層視窗系統的事件。
和上面很相似,只在GUI執行緒中使用QNetwork類。一個經常被問到的問題是一個QSocket是否可以在多執行緒中使用。這不是必須得,因為所有的QNetwork類都是非同步的。
不要在不是GUI執行緒的執行緒中試圖呼叫processEvents()函式。這也包括QDialog::exec()、QPopupMenu::exec()、QApplication::processEvents()和其它一些。
在你的應用程式中,不要把普通的Qt庫和支援執行緒的Qt庫混合使用。這也就是說如果你的程式使用了支援執行緒的Qt庫,你就不應該連線普通的Qt庫、動態的載入普通Qt庫或者動態地連線其它依賴普通Qt庫的庫或者外掛。在一些系統上,這樣做會導致Qt庫中使用的靜態資料變得不可靠了。

相關文章