線程概覽
使用線程
The QThread class provides a platform-independent way to manage threads.
A QThread object manages one thread of control within the program. QThreads begin executing in run(). By default, run() starts the event loop by calling exec() and runs a Qt event loop inside the thread.
我們使用線程的概率大于進(jìn)程,如何有效的使用線程進(jìn)行開發(fā),是我們學(xué)習(xí)的
一個(gè)重要的模塊。
一個(gè)實(shí)列
class WorkerThread : public QThread
{
Q_OBJECT
void run() Q_DECL_OVERRIDE {
QString result;
/* ... here is the expensive or blocking operation ... */
emit resultReady(result);
}
signals:
void resultReady(const QString &s);
};
void MyObject::startWorkInAThread()
{
WorkerThread *workerThread = new WorkerThread(this);
connect(workerThread, &WorkerThread::resultReady, this, &MyObject::handleResults);
connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater);
workerThread->start();
}
在這個(gè)例子中,線程將在運(yùn)行函數(shù)返回后退出。不會(huì)有任何事件循環(huán)運(yùn)行在線程,除非你把exec()。
這是要記住qthread實(shí)例在老線程實(shí)例化它的重要,不在新線程中調(diào)用run()。這意味著所有的排隊(duì)qthread插槽會(huì)在舊的線程執(zhí)行。因此,開發(fā)商們希望在新的線程調(diào)用槽必須使用工作對象的方法;新的槽不應(yīng)實(shí)施直接進(jìn)入子類qthread。
在子類qthread,記住,構(gòu)造函數(shù)執(zhí)行在舊線run()在新線程執(zhí)行時(shí)。如果從兩個(gè)函數(shù)中訪問成員變量,則該變量將從2個(gè)不同的線程中訪問。檢查它是否安全。
線程安全
- 可重入的(Reentrant):如果一個(gè)類允許多個(gè)線程使用它的多個(gè)實(shí)例,并且保證在同一時(shí)刻至多只有一個(gè)線程訪問同一個(gè)實(shí)例,就稱這個(gè)類是可重入的。大多數(shù) C++ 類都是可重入的。類似的,一個(gè)函數(shù)被稱為可重入的,如果該函數(shù)允許多個(gè)線程在同一時(shí)刻調(diào)用,而每一次的調(diào)用都只能使用其獨(dú)有的數(shù)據(jù)。全局變量就不是函數(shù)獨(dú)有的數(shù)據(jù),而是共享的。換句話說,這意味著類或者函數(shù)的使用者必須使用某種額外的機(jī)制(比如鎖)來控制對對象的實(shí)例或共享數(shù)據(jù)的序列化訪問。
- 線程安全(Thread-safe):如果多個(gè)線程可以在同一時(shí)刻使用一個(gè)類的對象,就說這個(gè)類是線程安全的。如果多個(gè)線程可以在同一時(shí)刻訪問函數(shù)的共享數(shù)據(jù),就稱這個(gè)函數(shù)是線程安全的。
線程阻塞
在worker努力工作的時(shí)候,事件循環(huán)在干什么?或許你已經(jīng)猜到了答案:什么都沒做!事件循環(huán)發(fā)出了鼠標(biāo)按下的事件,然后等著事件處理函數(shù)返回。此時(shí),它一直是阻塞的,直到Worker::doWork()函數(shù)結(jié)束。注意,我們使用了“阻塞”一詞,也就是說,所謂阻塞事件循環(huán),意思是沒有事件被派發(fā)處理。
在事件就此卡住時(shí), 組件也不會(huì)更新自身 (因?yàn)镼PaintEvent對象還在隊(duì)列中), 也不會(huì)有其它什么交互發(fā)生 (還是同樣的原因), 定時(shí)器也不會(huì)超時(shí) 并且 網(wǎng)絡(luò)交互會(huì)越來越慢直到停止 。也就是說,前面我們大費(fèi)周折分析的各種依賴事件循環(huán)的活動(dòng)都會(huì)停止。這時(shí)候,需要窗口管理器會(huì)檢測到你的應(yīng)用程序不再處理任何時(shí)間,于是 告訴用戶你的程序失去響應(yīng) 。這就是為什么我們需要快速地處理事件,并且盡可能快地返回事件循環(huán)。
# qt相關(guān)類
Qt 對線程的支持可以追溯到2000年9月22日發(fā)布的 Qt 2.2。在這個(gè)版本中,Qt 引入了QThread。不過,當(dāng)時(shí)對線程的支持并不是默認(rèn)開啟的。Qt 4.0 開始,線程成為所有平臺(tái)的默認(rèn)開啟選項(xiàng)
它也是 Qt 線程類中最核心的底層類。由于 Qt 的跨平臺(tái)特性,QThread要隱藏掉所有平臺(tái)相關(guān)的代碼。
正如前面所說,要使用QThread開始一個(gè)線程,我們可以創(chuàng)建它的一個(gè)子類,然后覆蓋其QThread::run()函數(shù)
QRunnable是我們要介紹的第二個(gè)類。這是一個(gè)輕量級的抽象類,用于開始一個(gè)另外線程的任務(wù)。這種任務(wù)是運(yùn)行過后就丟棄的。由于這個(gè)類是抽象類,我們需要繼承QRunnable,然后重寫其純虛函數(shù)QRunnable::run():
要真正執(zhí)行一個(gè)QRunnable對象,我們需要使用QThreadPool類。顧名思義,這個(gè)類用于管理一個(gè)線程池。通過調(diào)用QThreadPool::start(runnable)函數(shù),我們將一個(gè)QRunnable對象放入QThreadPool的執(zhí)行隊(duì)列。一旦有線程可用,線程池將會(huì)選擇一個(gè)QRunnable對象,然后在那個(gè)線程開始執(zhí)行。所有 Qt 應(yīng)用程序都有一個(gè)全局線程池,我們可以使用QThreadPool::globalInstance()獲得這個(gè)全局線程池;與此同時(shí),我們也可以自己創(chuàng)建私有的線程池,并進(jìn)行手動(dòng)管理。
需要注意的是,QRunnable不是一個(gè)QObject,因此也就沒有內(nèi)建的與其它組件交互的機(jī)制。為了與其它組件進(jìn)行交互,你必須自己編寫低級線程原語,例如使用 mutex 守護(hù)來獲取結(jié)果等。
QtConcurrent是我們要介紹的最后一個(gè)對象。這是一個(gè)高級 API,構(gòu)建于QThreadPool之上,用于處理大多數(shù)通用的并行計(jì)算模式:map、reduce 以及 filter。它還提供了QtConcurrent::run()函數(shù),用于在另外的線程運(yùn)行一個(gè)函數(shù)。注意,QtConcurrent是一個(gè)命名空間而不是一個(gè)類,因此其中的所有函數(shù)都是命名空間內(nèi)的全局函數(shù)。
不同于QThread和QRunnable,QtConcurrent不要求我們使用低級同步原語:所有的QtConcurrent都返回一個(gè)QFuture對象。這個(gè)對象可以用來查詢當(dāng)前的運(yùn)算狀態(tài)(也就是任務(wù)的進(jìn)度),可以用來暫停/回復(fù)/取消任務(wù),當(dāng)然也可以用來獲得運(yùn)算結(jié)果。QFutureWatcher類則用來監(jiān)視QFuture的進(jìn)度,我們可以用信號槽與QFutureWatcher進(jìn)行交互(注意,QFuture也沒有繼承QObject)。
線程與QObject
我們說,每一個(gè) Qt 應(yīng)用程序至少有一個(gè)事件循環(huán),就是調(diào)用了QCoreApplication::exec()的那個(gè)事件循環(huán)。不過,QThread也可以開啟事件循環(huán)。只不過這是一個(gè)受限于線程內(nèi)部的事件循環(huán)。因此我們將處于調(diào)用main()函數(shù)的那個(gè)線程,并且由QCoreApplication::exec()創(chuàng)建開啟的那個(gè)事件循環(huán)成為主事件循環(huán),或者直接叫主循環(huán)。注意,QCoreApplication::exec()只能在調(diào)用main()函數(shù)的線程調(diào)用。主循環(huán)所在的線程就是主線程,也被成為 GUI 線程,因?yàn)樗杏嘘P(guān) GUI 的操作都必須在這個(gè)線程進(jìn)行。QThread的局部事件循環(huán)則可以通過在QThread::run()中調(diào)用QThread::exec()開啟