Qt對線程提供了支援,基本形式有獨立於平台的線程類、安全執行緒方式的事件傳遞和一個全域Qt庫互斥量允許你可以從不同的線程調用Qt方法。
這個文檔是提供給那些對多線程編程有豐富的知識和經驗的聽眾的。推薦閱讀:
- Threads Primer: A Guide to Multithreaded Programming
- Thread Time: The Multithreaded Programming Guide
- Pthreads Programming: A POSIX Standard for Better Multiprocessing (O'Reilly Nutshell)
- Win32 Multithreaded Programming
警告:所有的GUI類(比如,QWidget和它的子類),作業系統核心類(比如,QProcess)和網路類都不是安全執行緒的。
QRegExp使用一個靜態緩衝並且也不是安全執行緒的,即使通過使用QMutex來保護的QRegExp對象。
線程類
最重要的類是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庫中使用的待用資料變得不可靠了。
QT通過三種形式提供了對線程的支援。它們分別是,一、平台無關的線程類,二、安全執行緒的事件投遞,三、跨線程的訊號-槽串連。這使得開發輕巧的多線程 Qt程式更為容易,並能充分利用多處理器機器的優勢。多線程編程也是一個有用的模式,它用於解決執行較長時間的操作而不至於使用者介面失去響應。
Qt 線程類
Qt 包含下面一些線程相關的類:
QThread 提供了開始一個新線程的方法
QThreadStorage 提供逐線程資料存放區
QMutex 提供相互排斥的鎖,或互斥量
QMutexLocker 是一個便利類,它可以自動對QMutex加鎖與解鎖
QReadWriteLock 提供了一個可以同時讀寫操作的鎖
QReadLocker與QWriteLocker 是便利類,它自動對QReadWriteLock加鎖與解鎖
QSemaphore 提供了一個整型訊號量,是互斥量的泛化
QWaitCondition 提供了一種方法,使得線程可以在被另外線程喚醒之前一直休眠。
Qt 進階線程類
QtConcurrent 開啟線程事務
QFutureWatcher 觀測線程狀態
QFuture 線程啟動類
QThread建立線程
為建立一個線程,子類化QThread並且重寫它的run()函數,例如:
class MyThread : public QThread
{
Q_OBJECT
protected:
void run();
};
void MyThread::run()
{
...
}
之後調用start,Qt即可建立一個線程,並線上程中執行run()函數中代碼,注意UI非安全執行緒的。
QtConcurrent建立線程
QtConcurrent 建立線程的方法比較多, 而且QtConcurrent 本身比較特殊,若系統有空閑線程時,它會調度空閑線程,無空閑線程時將會建立一個線程。
(注意:QtConcurrent 建立線程歸QthreadPool管理,若超過最大線程數,將會進入隊列等待),QtConcurrent建立線程的方法多種,以下舉例map函數:
QImage scale(const QImage &image)
{
qDebug() < < "Scaling image in thread" << QThread::currentThread();
return image.scaled(QSize(100, 100), Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
}
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
const int imageCount = 20;
// Create a list containing imageCount images.
QList images;
for (int i = 0; i < imageCount; ++i)
images.append(QImage(1600, 1200, QImage::Format_ARGB32_Premultiplied));
// Use QtConcurrentBlocking::mapped to apply the scale function to all the
// images in the list.
QList thumbnails = QtConcurrent::blockingMapped(images, scale);
return 0;
}
Qt 線程同步
QMutex, QReadWriteLock, QSemaphore, QWaitCondition 提供了線程同步的手段。使用線程的主要想法是希望它們可以儘可能並發執行,而一些關鍵點上線程之間需要停止或等待。例如,假如兩個線程試圖同時訪問同一個 全域變數,結果可能不如所願。
QMutex
QMutex 提供相互排斥的鎖,或互斥量。在一個時刻至多一個線程擁有mutex,假如一個線程試圖訪問已經被鎖定的mutex,那麼它將休眠,直到擁有mutex的線程對此mutex解鎖。Mutexes常用來保護共用資料訪問。
QReadWriterLock
QReadWriterLock 與QMutex相似,除了它對 “read”,”write”訪問進行區別對待。它使得多個讀者可以共時訪問資料。使用QReadWriteLock而不是QMutex,可以使得多線程程式更具有並發性。
QReadWriteLock lock;
void ReaderThread::run()
{
lock.lockForRead();
read_file();
lock.unlock();
}
void WriterThread::run()
{
lock.lockForWrite();
write_file();
lock.unlock();
}
QSemaphore
QSemaphore 是QMutex的一般化,它可以保護一定數量的相同資源,與此相對,一個mutex只保護一個資源。下面例子中,使用QSemaphore來控制對環狀緩 沖的訪問,此緩衝區被生產者線程和消費者線程共用。生產者不斷向緩衝寫入資料直到緩衝末端,再從頭開始。消費者從緩衝不斷讀取資料。訊號量比互斥量有更好 的並發性,假如我們用互斥量來控制對緩衝的訪問,那麼生產者,消費者不能同時訪問緩衝。然而,我們知道在同一時刻,不同線程訪問緩衝的不同部分並沒有什麼
危害。
const int DataSize = 100000;
const int BufferSize = 8192;
char buffer[BufferSize];
QSemaphore freeBytes(BufferSize);
QSemaphore usedBytes;
class Producer : public QThread
{
public:
void run();
};
void Producer::run()
{
qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
for (int i = 0; i < DataSize; ++i) {
freeBytes.acquire();
buffer[i % BufferSize] = "ACGT"[(int)qrand() % 4];
usedBytes.release();
}
}
class Consumer : public QThread
{
public:
void run();
};
void Consumer::run()
{
for (int i = 0; i < DataSize; ++i) {
usedBytes.acquire();
fprintf(stderr, "%c", buffer[i % BufferSize]);
freeBytes.release();
}
fprintf(stderr, "\n");
}
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
Producer producer;
Consumer consumer;
producer.start();
consumer.start();
producer.wait();
consumer.wait();
return 0;
}
QWaitCondition
QWaitCondition 允許線程在某些情況發生時喚醒另外的線程。一個或多個線程可以阻塞等待一QWaitCondition ,用wakeOne()或wakeAll()設定一個條件。wakeOne()隨機喚醒一個,wakeAll()喚醒所有。
下面的例子中,生產者首先必須檢查緩衝是否已滿(numUsedBytes==BufferSize),如果是,線程停下來等待 bufferNotFull條件。如果不是,在緩衝中生產資料,增加numUsedBytes,啟用條件 bufferNotEmpty。使用mutex來保護對numUsedBytes的訪問。另外,QWaitCondition::wait() 接收一個mutex作為參數,這個mutex應該被調用線程初始化為鎖定狀態。線上程進入休眠狀態之前,mutex會被解鎖。而當線程被喚醒
時,mutex會處於鎖定狀態,而且,從鎖定狀態到等待狀態的轉換是原子操作,這阻止了競爭條件的產生。當程式開始運行時,只有生產者可以工作。消費者被 阻塞等待bufferNotEmpty條件,一旦生產者在緩衝中放入一個位元組,bufferNotEmpty條件被激發,消費者線程於是被喚醒。
const int DataSize = 100000;
const int BufferSize = 8192;
char buffer[BufferSize];
QWaitCondition bufferNotEmpty;
QWaitCondition bufferNotFull;
QMutex mutex;
int numUsedBytes = 0;
class Producer : public QThread
{
public:
void run();
};
void Producer::run()
{
qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
for (int i = 0; i < DataSize; ++i) {
mutex.lock();
if (numUsedBytes == BufferSize)
bufferNotFull.wait(&mutex);
mutex.unlock();
buffer[i % BufferSize] = "ACGT"[(int)qrand() % 4];
mutex.lock();
++numUsedBytes;
bufferNotEmpty.wakeAll();
mutex.unlock();
}
}
class Consumer : public QThread
{
public:
void run();
};
void Consumer::run()
{
for (int i = 0; i < DataSize; ++i) {
mutex.lock();
if (numUsedBytes == 0)
bufferNotEmpty.wait(&mutex);
mutex.unlock();
fprintf(stderr, "%c", buffer[i % BufferSize]);
mutex.lock();
--numUsedBytes;
bufferNotFull.wakeAll();
mutex.unlock();
}
fprintf(stderr, "\n");
}
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
Producer producer;
Consumer consumer;
producer.start();
consumer.start();
producer.wait();
consumer.wait();
return 0;
}