使用線程c++
基本上有種使用線程的場合:編程
經過利用處理器的多個核使處理速度更快。設計模式
爲保持GUI線程或其餘高實時性線程的響應,將耗時的操做或阻塞的調用移到其餘線程。瀏覽器
什麼時候使用其餘技術替代線程開發人員使用線程時須要很是當心。安全
啓動線程是很容易的,但確保全部共享數據保持一致很難。遇到問題每每很難解決,這是因爲在一段時間內它可能只出現一次或只在特定的硬件配置下出現。在建立線程來解決某些問題以前,應該考慮一些替代的技術 :服務器
替代技術網絡
註解 QEventLoop::processEvents()數據結構
在一個耗時的計算操做中反覆調用QEventLoop::processEvents() 能夠防止界面的假死。儘管如此,這個方案可伸縮性並不太好,由於該函數可能會被調用地過於頻繁或者不夠頻繁。多線程
QTimer 後臺處理操做有時能夠方便地使用Timer安排在一個在將來的某一時刻執行的槽中來完成。在沒有其餘事件須要處理時,時間隔爲0的定時器超時事件被相應併發
QSocketNotifier QNetworkAccessManager QIODevice::readyRead()
這是一個替代技術,替代有一個或多個線程在慢速網絡執行阻塞讀的狀況。只要響應部分的計算能夠快速執行,這種設計比在線程中實現的同步等待更好。與線程相比這種設計更不容易出錯且更節能(energy efficient)。在許多狀況下也有性能優點。
通常狀況下,建議只使用安全和通過測試的方案而避免引入特設線程的概念。QtConcurrent 提供了一個將任務分發處處理器全部的核的易用接口。線程代碼徹底被隱藏在 QtConcurrent 框架下,因此你沒必要考慮細節。儘管如此,QtConcurrent 不能用於線程運行時須要通訊的狀況,並且它也不該該被用來處理阻塞操做。 應該使用 Qt 線程的哪一種技術?
有時候,你須要的不只僅是在另外一線程的上下文中運行一個函數。您可能須要有一個生存在另外一個線程中的對象來爲GUI線程提供服務。也許你想在另外一個始終運行的線程中來輪詢硬件端口並在有關注的事情發生時發送信號到GUI線程。
Qt爲開發多線程應用程序提供了多種不一樣的解決方案。解決方案的選擇依賴於新線程的目的以及線程的生命週期。
生命週期
開發任務
解決方案
一次調用
在另外一個線程中運行一個函數,函數完成時退出線程
編寫函數,使用QtConcurrent::run 運行它
派生QRunnable,使用QThreadPool::globalInstance()->start() 運行它
派生QThread,從新實現QThread::run() ,使用QThread::start() 運行它
一次調用
須要操做一個容器中全部的項。使用處理器全部可用的核心。一個常見的例子是從圖像列表生成縮略圖。
QtConcurrent 提供了map()函數來將操做應用到容器中的每個元素,提供了fitler()函數來選擇容器元素,以及指定reduce函數做爲選項來組合剩餘元素。
一次調用
一個耗時運行的操做須要放入另外一個線程。在處理過程當中,狀態信息須要發送會GUI線程。 使用QThread,從新實現run函數並根據須要發送信號。使用信號槽的queued鏈接方式將信號鏈接到GUI線程的槽函數。
持久運行
生存在另外一個線程中的對象,根據要求須要執行不一樣的任務。這意味着工做線程須要雙向的通信。 派生一個QObject對象並實現須要的信號和槽,將對象移動到一個運行有事件循環的線程中並經過queued方式鏈接的信號槽進行通信。
持久運行
生存在另外一個線程中的對象,執行諸如輪詢端口等重複的任務並與GUI線程通信。 同上,可是在工做線程中使用一個定時器來輪詢。儘管如此,處理輪詢的最好的解決方案是完全避免它。有時QSocketNotifer是一個替代。
Qt線程基礎
QThread是一個很是便利的跨平臺的對平臺原生線程的抽象。啓動一個線程是很簡單的。讓咱們看一個簡短的代碼:生成一個在線程內輸出"hello"並退出的線程。
// hellothread/hellothread.h
1
2
3
4
5
6
|
class
HelloThread :
public
QThread
{
Q_OBJECT
private
:
void
run();
};
|
咱們從QThread派生出一個類,並從新實現run方法。
// hellothread/hellothread.cpp
1
2
3
4
|
void
HelloThread::run()
{
qDebug() <<
"hello from worker thread "
<<
thread
()->currentThreadId();
}
|
run方法中包含將在另外一個線程中運行的代碼。在本例中,一個包含線程ID的消息被打印出來。 QThread::start() 將在另外一個線程中被調用。
1
2
3
4
5
6
7
8
9
|
int
main(
int
argc,
char
*argv[])
{
QCoreApplication app(argc, argv);
HelloThread
thread
;
thread
.start();
qDebug() <<
"hello from GUI thread "
<< app.
thread
()->currentThreadId();
thread
.wait();
// do not exit before the thread is completed!
return
0;
}
|
QObject與線程
QObject有線程關聯(thread affinity)[如何翻譯?關聯?依附性?dbzhang800 20110618],換句話說,它生存於一個特定的線程。這意味着,在建立時QObject保存了到當前線程的指針。當事件使用postEvent()被派發時,這個信息變得頗有用。事件被放置到相應線程的事件循環中。若是QObject所依附的線程沒有事件循環,該事件將永遠不會被傳遞。 要啓動事件循環,必須在run()內調用exec()。線程關聯能夠經過moveToThread()來更改。 如上所述,當從其餘線程調用對象的方法時開發人員必須始終保持謹慎。線程關聯不會改變這種情況。 Qt文檔中將一些方法標記爲線程安全。postEvent()就是一個值得注意的例子。一個線程安全的方法能夠同時在不一樣的線程被調用。 一般狀況下並不會併發訪問的一些方法,在其餘線程調用對象的非線程安全的方法在出現形成意想不到行爲的併發訪問前數千次的訪問可能都是工做正常的。編寫測試代碼不能徹底確保線程的正確性,但它仍然是重要的。在Linux上,Valgrind和Helgrind有助於檢測線程錯誤。 QThread的內部結構很是有趣: QThread並不生存於執行run()的新線程內。它生存於舊線程中。 QThread的大多數成員方法是線程的控制接口,並設計成從舊線程中被調用。不要使用moveToThread()將該接口移動到新建立的線程中;調用moveToThread(this)被視爲很差的實踐。 exec()和靜態方法usleep()、msleep()、sleep()要在新建立的線程中調用。 QThread子類中定義的其餘成員可在兩個線程中訪問。開發人員負責訪問的控制。一個典型的策略是在start()被調用前設置成員變量。一旦工做線程開始運行,主線程不該該操做其餘成員。當工做線程終止後,主線程能夠再次訪問其餘成員。這是一個在線程開始前傳遞參數並在結束後收集結果的便捷的策略。 QObject必須始終和parent在同一個線程。對於在run()中生成的對象這兒有一個驚人的後果:
1
2
3
4
5
6
|
void
HelloThread::run()
{
QObject *object1 =
new
QObject(
this
);
//error, parent must be in the same thread
QObject object2;
// OK
QSharedPointer <QObject> object3(
new
QObject);
// OK
}
|
使用互斥量保護數據的完整
互斥量是一個擁有lock()和unlock()方法並記住它是否已被鎖定的對象。互斥量被設計爲從多個線程調用。若是信號量未被鎖定lock()將當即返回。下一次從另外一個線程調用會發現該信號量處於鎖定狀態,而後lock()會阻塞線程直到其餘線程調用unlock()。此功能能夠確保代碼段將在同一時間只能由一個線程執行。 使用事件循環防止數據破壞
Qt的事件循環對線程間的通訊是一個很是有價值的工具。每一個線程均可以有它本身的事件循環。在另外一個線程中調用一個槽的一個安全的方法是將調用放置到另外一個線程的事件循環中。這能夠確保目標對象調用另外一個的成員函數以前能夠完成當前正在運行的成員函數。 那麼,如何才能把一個成員調用放於一個事件循環中? Qt的有兩種方法來作這個。一種方法是經過queued信號槽鏈接;另外一種是使用QCoreApplication::postEvent()派發一個事件。queued的信號槽鏈接是異步執行的信號槽鏈接。內部實現是基於posted的事件。信號的參數放入事件循環後信號函數的調用將當即返回。 鏈接的槽函數什麼時候被執行依賴於事件循環其餘的其餘操做。 經過事件循環通訊消除了咱們使用互斥量時所面臨的死鎖問題。這就是咱們爲何推薦使用事件循環,而不是使用互斥量鎖定對象的緣由。 處理異步執行
一種得到一個工做線程的結果的方法是等待線程終止。在許多狀況下,一個阻塞等待是不可接受的。阻塞等待的替代方法是異步的結果經過posted事件或者queued信號槽進行傳遞。因爲操做的結果不會出如今源代碼的下一行而是在位於源文件其餘部分的一個槽中,這會產生必定的開銷,由於,但在位於源文件中其餘地方的槽。 Qt開發人員習慣於使用這種異步行爲工做,由於它很是類似於GUI程序中使用的的事件驅動編程。 </h1>
http://www.devbean.net/2013/11/qt-study-road-2-thread-intro/
Qt 學習之路 :線程簡介
前面咱們討論了有關進程以及進程間通信的相關問題,如今咱們開始討論線程。事實上,現代的程序中,使用線程的機率應該大於進程。特別是在多核時代,隨着 CPU 主頻的提高,受制於發熱量的限制,CPU 散熱問題已經進入瓶頸,另闢蹊徑地提升程序運行效率就是使用線程,充分利用多核的優點。有關線程和進程的區別已經超出了本章的範疇,咱們簡單提一句,一個進程能夠有一個或更多線程同時運行。線程能夠看作是「輕量級進程」,進程徹底由操做系統管理,線程便可以由操做系統管理,也能夠由應用程序管理。 Qt 使用QThread 來管理線程。下面來看一個簡單的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
MainWindow::MainWindow(QWidget*parent)
:QMainWindow(parent)
{
QWidget *widget=
new
QWidget(
this
);
QVBoxLayout *layout=
new
QVBoxLayout;
widget->setLayout(layout);
QLCDNumber* lcdNumber=
new
QLCDNumber(
this
);
layout->addWidget(lcdNumber);
QPushButton* button=
new
QPushButton(tr(
"Start"
),
this
);
layout->addWidget(button);
setCentralWidget(widget);
QTimer *timer=
new
QTimer(
this
);
connect(timer,&QTimer::timeout,[=](){
staticintsec=0;
lcdNumber->display(QString::number(sec++));
});
WorkerThread *
thread
=
new
WorkerThread(
this
);
connect(button,&QPushButton::clicked,[=](){
timer->start(1);
for
(inti=0;i<2000000000;i++);
timer->stop();
});
}
|
咱們的主界面有一個用於顯示時間的 LCD 數字面板還有一個用於啓動任務的按鈕。程序的目的是用戶點擊按鈕,開始一個很是耗時的運算(程序中咱們以一個 2000000000 次的循環來替代這個很是耗時的工做,在真實的程序中,這多是一個網絡訪問,多是須要複製一個很大的文件或者其它任務),同時 LCD 開始顯示逝去的毫秒數。毫秒數經過一個計時器QTimer進行更新。計算完成後,計時器中止。這是一個很簡單的應用,也看不出有任何問題。可是當咱們開始運行程序時,問題就來了:點擊按鈕以後,程序界面直接中止響應,直到循環結束纔開始從新更新。 有經驗的開發者當即指出,這裏須要使用線程。這是由於 Qt 中全部界面都是在 UI 線程中(也被稱爲主線程,就是執行了QApplication::exec()的線程),在這個線程中執行耗時的操做(好比那個循環),就會阻塞 UI 線程,從而讓界面中止響應。界面中止響應,用戶體驗天然很差,不過更嚴重的是,有些窗口管理程序會檢測到你的程序已經失去響應,可能會建議用戶強制中止程序,這樣一來你的程序可能就此終止,任務再也沒法完成。因此,爲了不這一問題,咱們要使用 QThread 開啓一個新的線程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
class
WorkerThread:
public
QThread
{
Q_OBJECT
public
:
WorkerThread(QObject*parent=0)
:QThread(parent)
{
}
protected
:
void
run()
{
for
(inti=0;i<1000000000;i++);
emit done();
}
signals:
voiddone();
};
MainWindow::MainWindow(QWidget*parent)
:QMainWindow(parent)
{
QWidget *widget=
new
QWidget(
this
);
QVBoxLayout *layout=
new
QVBoxLayout;
widget->setLayout(layout);
lcdNumber=
new
QLCDNumber(
this
);
layout->addWidget(lcdNumber);
QPushButton *button=
new
QPushButton(tr(
"Start"
),
this
);
layout->addWidget(button);
setCentralWidget(widget);
QTimer *timer=
new
QTimer(
this
);
connect(timer,&QTimer::timeout,[=](){
staticintsec=0;
lcdNumber->display(QString::number(sec++));
});
WorkerThread *
thread
=
new
WorkerThread(
this
);
connect(
thread
,&WorkerThread::done,timer,&QTimer::stop);
connect(
thread
,&WorkerThread::finished,
thread
,&WorkerThread::deleteLater);
connect(button,&QPushButton::clicked,[=]()
{
timer->start(1);
thread
->start();
});
}
|
注意,咱們增長了一個WorkerThread類。WorkerThread繼承自QThread類,重寫了其run()函數。咱們能夠認爲,run()函數就是新的線程須要執行的代碼。在這裏就是要執行這個循環,而後發出計算完成的信號。而在按鈕點擊的槽函數中,使用QThread::start()函數啓動一個線程(注意,這裏不是run()函數)。再次運行程序,你會發現如今界面已經不會被阻塞了。另外,咱們將WorkerThread::deleteLater()函數與WorkerThread::finished()信號鏈接起來,當線程完成時,系統能夠幫咱們清除線程實例。這裏的finished()信號是系統發出的,與咱們自定義的done()信號無關。 這是 Qt 線程的最基本的使用方式之一(確切的說,這種使用已經不大推薦使用,不過由於看起來很清晰,並且簡單使用起來也沒有什麼問題,因此仍是有必要介紹)。代碼看起來很簡單,不過,若是你認爲 Qt 的多線程編程也很簡單,那就大錯特錯了。Qt 多線程的優點設計使得它使用起來變得容易,可是坑不少,稍不留神就會被絆住,尤爲是涉及到與 QObject 交互的狀況。稍懂多線程開發的童鞋都會知道,調試多線程開發簡直就是煎熬。下面幾章,咱們會更詳細介紹有關多線程編程的相關內容。
http://www.devbean.net/2013/11/qt-study-road-2-thread-and-event-loop/
Qt 學習之路 :線程和事件循環
前面一章咱們簡單介紹瞭如何使用QThread實現線程。如今咱們開始詳細介紹如何「正確」編寫多線程程序。咱們這裏的大部份內容來自於Qt的一篇Wiki文檔,有興趣的童鞋能夠去看原文。 在介紹在之前,咱們要認識兩個術語: 可重入的(Reentrant):若是多個線程能夠在同一時刻調用一個類的全部函數,而且保證每一次函數調用都引用一個惟一的數據,就稱這個類是可重入的(Reentrant means that all the functions in the referenced class can be called simultaneously by multiple threads, provided that each invocation of the functions reference unique data.)。大多數 C++ 類都是可重入的。相似的,一個函數被稱爲可重入的,若是該函數容許多個線程在同一時刻調用,而每一次的調用都只能使用其獨有的數據。全局變量就不是函數獨有的數據,而是共享的。換句話說,這意味着類或者函數的使用者必須使用某種額外的機制(好比鎖)來控制對對象的實例或共享數據的序列化訪問。 線程安全(Thread-safe):若是多個線程能夠在同一時刻調用一個類的全部函數,即便每一次函數調用都引用一個共享的數據,就說這個類是線程安全的(Threadsafe means that all the functions in the referenced class can be called simultaneously by multiple threads even when each invocation references shared data.)。若是多個線程能夠在同一時刻訪問函數的共享數據,就稱這個函數是線程安全的。 進一步說,對於一個類,若是不一樣的實例能夠被不一樣線程同時使用而不受影響,就說這個類是可重入的;若是這個類的全部成員函數均可以被不一樣線程同時調用而不受影響,即便這些調用針對同一個對象,那麼咱們就說這個類是線程安全的。由此能夠看出,線程安全的語義要強於可重入。接下來,咱們從事件開始討論。以前咱們說過,Qt 是事件驅動的。在 Qt 中,事件由一個普通對象表示(QEvent或其子類)。這是事件與信號的一個很大區別:事件老是由某一種類型的對象表示,針對某一個特殊的對象,而信號則沒有這種目標對象。全部QObject的子類均可以經過覆蓋QObject::event()函數來控制事件的對象。 事件能夠由程序生成,也能夠在程序外部生成。例如: QKeyEvent和QMouseEvent對象表示鍵盤或鼠標的交互,一般由系統的窗口管理器產生; QTimerEvent事件在定時器超時時發送給一個QObject,定時器事件一般由操做系統發出; QChildEvent在增長或刪除子對象時發送給一個QObject,這是由 Qt 應用程序本身發出的。 須要注意的是,與信號不一樣,事件並非一產生就被分發。事件產生以後被加入到一個隊列中(這裏的隊列含義同數據結構中的概念,先進先出),該隊列即被稱爲事件隊列。事件分發器遍歷事件隊列,若是發現事件隊列中有事件,那麼就把這個事件發送給它的目標對象。這個循環被稱做事件循環。事件循環的僞代碼描述大體以下所示:
1
2
3
4
5
6
7
8
|
while
(is_active)
{
while
(!event_queue_is_empty)
{
dispatch_next_event();
}
wait_for_more_events();
}
|
正如前面所說的,調用QCoreApplication::exec() 函數意味着進入了主循環。咱們把事件循環理解爲一個無限循環,直到QCoreApplication::exit()或者QCoreApplication::quit()被調用,事件循環才真正退出。 僞代碼裏面的while會遍歷整個事件隊列,發送從隊列中找到的事件;wait_for_more_events()函數則會阻塞事件循環,直到又有新的事件產生。咱們仔細考慮這段代碼,在wait_for_more_events()函數所獲得的新的事件都應該是由程序外部產生的。由於全部內部事件都應該在事件隊列中處理完畢了。所以,咱們說事件循環在wait_for_more_events()函數進入休眠,而且能夠被下面幾種狀況喚醒: 窗口管理器的動做(鍵盤、鼠標按鍵按下、與窗口交互等); 套接字動做(網絡傳來可讀的數據,或者是套接字非阻塞寫等); 定時器; 由其它線程發出的事件(咱們會在後文詳細解釋這種狀況)。 在類 UNIX 系統中,窗口管理器(好比 X11)會經過套接字(Unix Domain 或 TCP/IP)嚮應用程序發出窗口活動的通知,由於客戶端就是經過這種機制與 X 服務器交互的。若是咱們決定要實現基於內部的socketpair(2)函數的跨線程事件的派發,那麼窗口的管理活動須要喚醒的是: 套接字 socket 定時器 timer 這也正是select(2)系統調用所作的:它監視窗口活動的一組描述符,若是在必定時間內沒有活動,它會發出超時消息(這種超時是可配置的)。Qt 所要作的,就是把select()的返回值轉換成一個合適的QEvent子類的對象,而後將其放入事件隊列。好了,如今你已經知道事件循環的內部機制了。 至於爲何須要事件循環,咱們能夠簡單列出一個清單: 組件的繪製與交互:QWidget::paintEvent()會在發出QPaintEvent事件時被調用。該事件能夠經過內部QWidget::update()調用或者窗口管理器(例如顯示一個隱藏的窗口)發出。全部交互事件(鍵盤、鼠標)也是相似的:這些事件都要求有一個事件循環才能發出。 定時器:長話短說,它們會在select(2)或其餘相似的調用超時時被髮出,所以你須要容許 Qt 經過返回事件循環來實現這些調用。 網絡:全部低級網絡類(QTcpSocket、QUdpSocket以及QTcpServer等)都是異步的。當你調用read()函數時,它們僅僅返回已可用的數據;當你調用write()函數時,它們僅僅將寫入列入計劃列表稍後執行。只有返回事件循環的時候,真正的讀寫纔會執行。注意,這些類也有同步函數(以waitFor開頭的函數),可是它們並不推薦使用,就是由於它們會阻塞事件循環。高級的類,例如QNetworkAccessManager則根本不提供同步 API,所以必需要求事件循環。 有了事件循環,你就會想怎樣阻塞它。阻塞它的理由可能有不少,例如我就想讓QNetworkAccessManager同步執行。在解釋爲何永遠不要阻塞事件循環以前,咱們要了解究竟什麼是「阻塞」。假設咱們有一個按鈕Button,這個按鈕在點擊時會發出一個信號。這個信號會與一個Worker對象鏈接,這個Worker對象會執行很耗時的操做。當點擊了按鈕以後,咱們觀察從上到下的函數調用堆棧:
1
2
3
4
5
6
7
8
|
main(
int
,
char
**)
QApplication::exec()
[…]
QWidget::event(QEvent *)
Button::mousePressEvent(QMouseEvent *)
Button::clicked()
[…]
Worker::doWork()
|
咱們在main()函數開始事件循環,也就是常見的QApplication::exec()函數。窗口管理器偵測到鼠標點擊後,Qt 會發現並將其轉換成QMouseEvent事件,發送給組件的event()函數。這一過程是經過QApplication::notify()函數實現的。注意咱們的按鈕並無覆蓋event()函數,所以其父類的實現將被執行,也就是QWidget::event()函數。這個函數發現這個事件是一個鼠標點擊事件,因而調用了對應的事件處理函數,就是Button::mousePressEvent()函數。咱們重寫了這個函數,發出Button::clicked()信號,而正是這個信號會調用Worker::doWork()槽函數。有關這一機制咱們在前面的事件部分曾有闡述,若是不明白這部分機制,請參考前面的章節。 在worker努力工做的時候,事件循環在幹什麼?或許你已經猜到了答案:什麼都沒作!事件循環發出了鼠標按下的事件,而後等着事件處理函數返回。此時,它一直是阻塞的,直到Worker::doWork()函數結束。注意,咱們使用了「阻塞」一詞,也就是說,所謂阻塞事件循環,意思是沒有事件被派發處理。 在事件就此卡住時,組件也不會更新自身(由於QPaintEvent對象還在隊列中),也不會有其它什麼交互發生(仍是一樣的緣由),定時器也不會超時而且網絡交互會愈來愈慢直到中止。也就是說,前面咱們大費周折分析的各類依賴事件循環的活動都會中止。這時候,須要窗口管理器會檢測到你的應用程序再也不處理任何事件,因而告訴用戶你的程序失去響應。這就是爲何咱們須要快速地處理事件,而且儘量快地返回事件循環。 如今,重點來了:咱們不可能避免業務邏輯中的耗時操做,那麼怎樣作才能既能夠執行那些耗時的操做,又不會阻塞事件循環呢?通常會有三種解決方案:第一,咱們將任務移到另外的線程(正如咱們上一章看到的那樣,不過如今咱們暫時略過這部份內容);第二,咱們手動強制運行事件循環。想要強制運行事件循環,咱們須要在耗時的任務中一遍遍地調用QCoreApplication::processEvents()函數。QCoreApplication::processEvents()函數會發出事件隊列中的全部事件,而且當即返回到調用者。仔細想一下,咱們在這裏所作的,就是模擬了一個事件循環。 另一種解決方案咱們在前面的章節提到過:使用QEventLoop類從新進入新的事件循環。經過調用QEventLoop::exec()函數,咱們從新進入新的事件循環,給QEventLoop::quit()槽函數發送信號則退出這個事件循環。拿前面的例子來講:
1
2
3
4
5
6
|
QEventLoop eventLoop;
connect(netWorker,&NetWorker::finished,
&eventLoop,&QEventLoop::quit);
QNetworkReply *reply=netWorker->get(url);
replyMap.insert(reply,FetchWeatherInfo);
eventLoop.exec();
|
QNetworkReply沒有提供阻塞式 API,而且要求有一個事件循環。咱們經過一個局部的QEventLoop來達到這一目的:當網絡響應完成時,這個局部的事件循環也會退出。 前面咱們也強調過:經過「其它的入口」進入事件循環要特別當心:由於它會致使遞歸調用!如今咱們能夠看看爲何會致使遞歸調用了。回過頭來看看按鈕的例子。當咱們在Worker::doWork()槽函數中調用了QCoreApplication::processEvents()函數時,用戶再次點擊按鈕,槽函數Worker::doWork()又一次被調用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
main(
int
,
char
**)
QApplication::exec()
[…]
QWidget::event(QEvent *)
Button::mousePressEvent(QMouseEvent *)
Button::clicked()
[…]
Worker::doWork()
// <strong>第一次調用</strong>
QCoreApplication::processEvents()
// <strong>手動發出全部事件</strong>
[…]
QWidget::event(QEvent *)
// <strong>用戶又點擊了一下按鈕…</strong>
Button::mousePressEvent(QMouseEvent *)
Button::clicked()
// <strong>又發出了信號…</strong>
[…]
Worker::doWork()
// <strong>遞歸進入了槽函數!</strong>
|
固然,這種狀況也有解決的辦法:咱們能夠在調用QCoreApplication::processEvents()函數時傳入QEventLoop::ExcludeUserInputEvents參數,意思是不要再次派發用戶輸入事件(這些事件仍舊會保留在事件隊列中)。 幸運的是,在刪除事件(也就是由QObject::deleteLater()函數加入到事件隊列中的事件)中,沒有這個問題。這是由於刪除事件是由另外的機制處理的。刪除事件只有在事件循環有比較小的「嵌套」的狀況下才會被處理,而不是調用了deleteLater()函數的那個循環。例如:
1
2
3
4
|
QObject* object=
new
QObject;
object->deleteLater();
QDialog dialog;
dialog.exec();
|
這段代碼並不會形成野指針(注意,QDialog::exec()的調用是嵌套在deleteLater()調用所在的事件循環以內的)。經過QEventLoop進入局部事件循環也是相似的。在 Qt 4.7.3 中,惟一的例外是,在沒有事件循環的狀況下直接調用deleteLater()函數,那麼,以後第一個進入的事件循環會獲取這個事件,而後直接將這個對象刪除。不過這也是合理的,由於 Qt 原本不知道會執行刪除操做的那個「外部的」事件循環,因此第一個事件循環就會直接刪除對象。
http://www.devbean.net/2013/11/qt-study-road-2-thread-related-classes/
Qt 學習之路 2(73):Qt 線程相關類
但願上一章有關事件循環的內容尚未把你繞暈。本章將從新回到有關線程的相關內容上面來。在前面的章節咱們瞭解了有關QThread類的簡單使用。不過,Qt 提供的有關線程的類可不那麼簡單,不然的話咱們也不必再三強調使用線程必定要萬分當心,一不留神就會陷入陷阱。 事實上,Qt 對線程的支持能夠追溯到2000年9月22日發佈的 Qt 2.2。在這個版本中,Qt 引入了QThread。不過,當時對線程的支持並非默認開啓的。Qt 4.0 開始,線程成爲全部平臺的默認開啓選項(這意味着若是不須要線程,你能夠經過編譯選項關閉它,不過這不是咱們如今的重點)。如今版本的 Qt 引入了不少類來支持線程,下面咱們將開始逐一瞭解它們。 QThread是咱們將要詳細介紹的第一個類。它也是 Qt 線程類中最核心的底層類。因爲 Qt 的跨平臺特性,QThread要隱藏掉全部平臺相關的代碼。 正如前面所說,要使用QThread開始一個線程,咱們能夠建立它的一個子類,而後覆蓋其QThread::run()函數:
1
2
3
4
5
6
7
8
|
class
Thread:
public
QThread
{
protected
:
voidrun()
{
/* 線程的相關代碼 */
}
};
|
而後咱們這樣使用新建的類來開始一個新的線程:
1
2
|
Thread*
thread
=
new
Thread;
thread
->start();
// 使用 start() 開始新的線程
|
注意,從 Qt 4.4 開始,QThread就已經不是抽象類了。QThread::run()再也不是純虛函數,而是有了一個默認的實現。這個默認實現實際上是簡單地調用了QThread::exec()函數,而這個函數,按照咱們前面所說的,實際上是開始了一個事件循環(有關這種實現的進一步闡述,咱們將在後面的章節詳細介紹)。 QRunnable是咱們要介紹的第二個類。這是一個輕量級的抽象類,用於開始一個另外線程的任務。這種任務是運行事後就丟棄的。因爲這個類是抽象類,咱們須要繼承QRunnable,而後重寫其純虛函數QRunnable::run():
1
2
3
4
5
6
7
8
|
class
Task:
public
QRunnable
{
public
:
void
run()
{
/* 線程的相關代碼 */
}
};
|
要真正執行一個QRunnable對象,咱們須要使用QThreadPool類。顧名思義,這個類用於管理一個線程池。經過調用QThreadPool::start(runnable)函數,咱們將一個QRunnable對象放入QThreadPool的執行隊列。一旦有線程可用,線程池將會選擇一個QRunnable對象,而後在那個線程開始執行。全部 Qt 應用程序都有一個全局線程池,咱們可使用QThreadPool::globalInstance()得到這個全局線程池;與此同時,咱們也能夠本身建立私有的線程池,並進行手動管理。 須要注意的是,QRunnable不是一個QObject,所以也就沒有內建的與其它組件交互的機制。爲了與其它組件進行交互,你必須本身編寫低級線程原語,例如使用 mutex 守護來獲取結果等。 QtConcurrent是咱們要介紹的最後一個對象。這是一個高級 API,構建於QThreadPool之上,用於處理大多數通用的並行計算模式:map、reduce 以及 filter。它還提供了QtConcurrent::run()函數,用於在另外的線程運行一個函數。注意,QtConcurrent是一個命名空間而不是一個類,所以其中的全部函數都是命名空間內的全局函數。 不一樣於QThread和QRunnable,QtConcurrent不要求咱們使用低級同步原語:全部的QtConcurrent都返回一個QFuture對象。這個對象能夠用來查詢當前的運算狀態(也就是任務的進度),能夠用來暫停/回覆/取消任務,固然也能夠用來得到運算結果。注意,並非全部的QFuture對象都支持暫停或取消的操做。好比,由QtConcurrent::run()返回的QFuture對象不能取消,可是由QtConcurrent::mappedReduced()返回的是能夠的。QFutureWatcher類則用來監視QFuture的進度,咱們能夠用信號槽與QFutureWatcher進行交互(注意,QFuture也沒有繼承QObject)。
http://www.devbean.net/2013/12/qt-study-road-2-thread-and-qobject/
Qt 學習之路 2(74):線程和 QObject
前面兩個章節咱們從事件循環和線程類庫兩個角度闡述有關線程的問題。本章咱們將深刻線程間得交互,探討線程和QObject之間的關係。在某種程度上,這纔是多線程編程真正須要注意的問題。 如今咱們已經討論過事件循環。咱們說,每個 Qt 應用程序至少有一個事件循環,就是調用了QCoreApplication::exec()的那個事件循環。不過,QThread也能夠開啓事件循環。只不過這是一個受限於線程內部的事件循環。所以咱們將處於調用main()函數的那個線程,而且由QCoreApplication::exec()建立開啓的那個事件循環成爲主事件循環,或者直接叫主循環。注意,QCoreApplication::exec()只能在調用main()函數的線程調用。主循環所在的線程就是主線程,也被成爲 GUI 線程,由於全部有關 GUI 的操做都必須在這個線程進行。QThread的局部事件循環則能夠經過在QThread::run()中調用QThread::exec()開啓:
1
2
3
4
5
6
7
8
|
class
Thread:
public
QThread
{
protected
:
voidrun(){
/* ... 初始化 ... */
exec();
}
};
|
記得咱們前面介紹過,Qt 4.4 版本之後,QThread::run()再也不是純虛函數,它會調用QThread::exec()函數。與QCoreApplication同樣,QThread也有QThread::quit()和QThread::exit()函數來終止事件循環。 線程的事件循環用於爲線程中的全部QObjects對象分發事件;默認狀況下,這些對象包括線程中建立的全部對象,或者是在別處建立完成後被移動到該線程的對象(咱們會在後面詳細介紹「移動」這個問題)。咱們說,一個QObject的所依附的線程(thread affinity)是指它所在的那個線程。它一樣適用於在QThread的構造函數中構建的對象:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class
MyThread:
public
QThread
{
public
:
MyThread()
{
otherObj=
new
QObject;
}
private
:
QObject obj;
QObject* otherObj;
QScopedPointer yetAnotherObj;
};
|
在咱們建立了MyThread對象以後,obj、otherObj和yetAnotherObj的線程依附性是怎樣的?是否是就是MyThread所表示的那個線程?要回答這個問題,咱們必須看看到底是哪一個線程建立了它們:實際上,是調用了MyThread構造函數的線程建立了它們。所以,這些對象不在MyThread所表示的線程,而是在建立了MyThread的那個線程中。 咱們能夠經過調用QObject::thread()能夠查詢一個QObject的線程依附性。注意,在QCoreApplication對象以前建立的QObject沒有所謂線程依附性,所以也就沒有對象爲其派發事件。也就是說,實際是QCoreApplication建立了表明主線程的QThread對象。
咱們可使用線程安全的QCoreApplication::postEvent()函數向一個對象發送事件。它將把事件加入到對象所在的線程的事件隊列中,所以,若是這個線程沒有運行事件循環,這個事件也不會被派發。 值得注意的一點是,QObject及其全部子類都不是線程安全的(但都是可重入的)。所以,你不能有兩個線程同時訪問一個QObject對象,除非這個對象的內部數據都已經很好地序列化(例如爲每一個數據訪問加鎖)。記住,在你從另外的線程訪問一個對象時,它可能正在處理所在線程的事件循環派發的事件!基於一樣的緣由,你也不能在另外的線程直接delete一個QObject對象,相反,你須要調用QObject::deleteLater()函數,這個函數會給對象所在線程發送一個刪除的事件。 此外,QWidget及其子類,以及全部其它 GUI 相關類(即使不是QObject的子類,例如QPixmap),甚至不是可重入的:它們只能在 GUI 線程訪問。 QObject的線程依附性是能夠改變的,方法是調用QObject::moveToThread()函數。該函數會改變一個對象及其全部子對象的線程依附性。因爲QObject不是線程安全的,因此咱們只能在該對象所在線程上調用這個函數。也就是說,咱們只能在對象所在線程將這個對象移動到另外的線程,不能在另外的線程改變對象的線程依附性。還有一點是,Qt 要求QObject的全部子對象都必須和其父對象在同一線程。這意味着: 不能對有父對象(parent 屬性)的對象使用QObject::moveToThread()函數 不能在QThread中以這個QThread自己做爲父對象建立對象,例如:
1
2
3
4
5
6
7
|
classThread:publicQThread
{
voidrun()
{
QObject *obj=
new
QObject(
this
);
// 錯誤!
}
};
|
這是由於QThread對象所依附的線程是建立它的那個線程,而不是它所表明的線程。 Qt 還要求,在表明一個線程的QThread對象銷燬以前,全部在這個線程中的對象都必須先delete。要達到這一點並不困難:咱們只需在QThread::run()的棧上建立對象便可。 如今的問題是,既然線程建立的對象都只能在函數棧上,怎麼能讓這些對象與其它線程的對象通訊呢?Qt 提供了一個優雅清晰的解決方案:咱們在線程的事件隊列中加入一個事件,而後在事件處理函數中調用咱們所關心的函數。顯然這須要線程有一個事件循環。這種機制依賴於 moc 提供的反射:所以,只有信號、槽和使用Q_INVOKABLE宏標記的函數能夠在另外的線程中調用。 QMetaObject::invokeMethod()靜態函數會這樣調用:
1
2
3
4
|
QMetaObject::invokeMethod(object,
"methodName"
,
Qt::QueuedConnection,
Q_ARG(type1,arg1),
Q_ARG(type2,arg2));
|
主意,上面函數調用中出現的參數類型都必須提供一個公有構造函數,一個公有的析構函數和一個公有的複製構造函數,而且要使用qRegisterMetaType()函數向 Qt 類型系統註冊。 跨線程的信號槽也是相似的。當咱們將信號與槽鏈接起來時,QObject::connect()的最後一個參數將指定鏈接類型: Qt::DirectConnection:直接鏈接意味着槽函數將在信號發出的線程直接調用 Qt::QueuedConnection:隊列鏈接意味着向接受者所在線程發送一個事件,該線程的事件循環將得到這個事件,而後以後的某個時刻調用槽函數 Qt::BlockingQueuedConnection:阻塞的隊列鏈接就像隊列鏈接,可是發送者線程將會阻塞,直到接受者所在線程的事件循環得到這個事件,槽函數被調用以後,函數纔會返回 Qt::AutoConnection:自動鏈接(默認)意味着若是接受者所在線程就是當前線程,則使用直接鏈接;不然將使用隊列鏈接 注意在上面每種狀況中,發送者所在線程都是可有可無的!在自動鏈接狀況下,Qt 須要查看信號發出的線程是否是與接受者所在線程一致,來決定鏈接類型。注意,Qt 檢查的是信號發出的線程,而不是信號發出的對象所在的線程!咱們能夠看看下面的代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class
Thread:
public
QThread
{
Q_OBJECT
signals:
void
aSignal();
protected
:
void
run()
{
emit aSignal();
}
};
/* ... */
Thread
thread
;
Object obj;
QObject::connect(&
thread
,SIGNAL(aSignal()),&obj,SLOT(aSlot()));
thread
.start();
|
aSignal()信號在一個新的線程被髮出(也就是Thread所表明的線程)。注意,由於這個線程並非Object所在的線程(Object所在的線程和Thread所在的是同一個線程,回憶下,信號槽的鏈接方式與發送者所在線程無關),因此這裏將會使用隊列鏈接。 另一個常見的錯誤是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
class
Thread:
public
QThread
{
Q_OBJECT
slots:
void
aSlot()
{
/* ... */
}
protected
:
void
run()
{
/* ... */
}
};
/* ... */
Thread
thread
;
Object obj;
QObject::connect(&obj,SIGNAL(aSignal()),&
thread
,SLOT(aSlot()));
thread
.start();
obj.emitSignal();
|
這裏的obj發出aSignal()信號時,使用哪一種鏈接方式?答案是:直接鏈接。由於Thread對象所在線程發出了信號,也就是信號發出的線程與接受者是同一個。在aSlot()槽函數中,咱們能夠直接訪問Thread的某些成員變量,可是注意,在咱們訪問這些成員變量時,Thread::run()函數可能也在訪問!這意味着兩者併發進行:這是一個完美的致使崩潰的隱藏bug。 另一個例子可能更爲重要:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
class
Thread:
public
QThread
{
Q_OBJECT
slots:
void
aSlot()
{
/* ... */
}
protected
:
void
run()
{
QObject*obj=newObject;
connect(obj,SIGNAL(aSignal()),
this
,SLOT(aSlot()));
/* ... */
}
};
|
這個例子也會使用隊列鏈接。然而,這個例子比上面的例子更具隱蔽性:在這個例子中,你可能會以爲,Object所在Thread所表明的線程中被建立,又是訪問的Thread本身的成員數據。稍有不慎便會寫出這種代碼。 爲了解決這個問題,咱們能夠這麼作:Thread構造函數中增長一個函數調用:moveToThread(this):
1
2
3
4
5
6
7
8
9
10
11
|
class
Thread:
public
QThread
{
Q_OBJECT
public
:
Thread()
{
moveToThread(
this
);
// 錯誤!
}
/* ... */
};
|
實際上,這的確可行(由於Thread的線程依附性被改變了:它所在的線程成了本身),可是這並非一個好主意。這種代碼意味着咱們其實誤解了線程對象(QThread子類)的設計意圖:QThread對象不是線程自己,它們實際上是用於管理它所表明的線程的對象。所以,它們應該在另外的線程被使用(一般就是它本身所在的線程),而不是在本身所表明的線程中。 上面問題的最好的解決方案是,將處理任務的部分與管理線程的部分分離。簡單來講,咱們能夠利用一個QObject的子類,使用QObject::moveToThread()改變其線程依附性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
class
Worker:
public
QObject
{
Q_OBJECT
public
slots:
void
doWork()
{
/* ... */
}
};
/* ... */
QThread *
thread
=
new
QThread;
Worker *worker=
new
Worker;
connect(obj,SIGNAL(workReady()),worker,SLOT(doWork()));
worker->moveToThread(
thread
);
thread
->start();
|
http://www.devbean.net/2013/12/qt-study-road-2-thread-summary/
Qt 學習之路:線程總結
前面咱們已經詳細介紹過有關線程的一些值得注意的事項。如今咱們開始對線程作一些總結。 有關線程,你能夠作的是: 在QThread子類添加信號。這是絕對安全的,而且也是正確的(前面咱們已經詳細介紹過,發送者的線程依附性沒有關係)
不該該作的是: 調用moveToThread(this)函數 指定鏈接類型:這一般意味着你正在作錯誤的事情,好比將QThread控制接口與業務邏輯混雜在了一塊兒(而這應該放在該線程的一個獨立對象中) 在QThread子類添加槽函數:這意味着它們將在錯誤的線程被調用,也就是QThread對象所在線程,而不是QThread對象管理的線程。這又須要你指定鏈接類型或者調用moveToThread(this)函數 使用QThread::terminate()函數 不能作的是: 在線程還在運行時退出程序。使用QThread::wait()函數等待線程結束 在QThread對象所管理的線程仍在運行時就銷燬該對象。若是你須要某種「自行銷燬」的操做,你能夠把finished()信號同deleteLater()槽鏈接起來 那麼,下面一個問題是:我何時應該使用線程? 首先,當你不得不使用同步 API 的時候。 若是你須要使用一個沒有非阻塞 API 的庫或代碼(所謂非阻塞 API,很大程度上就是指信號槽、事件、回調等),那麼,避免事件循環被阻塞的解決方案就是使用進程或者線程。不過,因爲開啓一個新的工做進程,讓這個進程去完成任務,而後再與當前進程進行通訊,這一系列操做的代價都要比開啓線程要昂貴得多,因此,線程一般是最好的選擇。 一個很好的例子是地址解析服務。注意咱們這裏並不討論任何第三方 API,僅僅假設一個有這樣功能的庫。這個庫的工做是將一個主機名轉換成地址。這個過程須要去到一個系統(也就是域名系統,Domain Name System, DNS)執行查詢,這個系統一般是一個遠程系統。通常這種響應應該瞬間完成,可是並不排除遠程服務器失敗、某些包可能會丟失、網絡可能失去連接等等。簡單來講,咱們的查詢可能會等幾十秒鐘。 UNIX 系統上的標準 API 是阻塞的(不只是舊的gethostbyname(3),就連新的getservbyname(3)和getaddrinfo(3)也是同樣)。Qt 提供的QHostInfo類一樣用於地址解析,默認狀況下,內部使用一個QThreadPool提供後臺運行方式的查詢(若是關閉了 Qt 的線程支持,則提供阻塞式 API)。 另一個例子是圖像加載和縮放。QImageReader和QImage只提供了阻塞式 API,容許咱們從設備讀取圖片,或者是縮放到不一樣的分辨率。若是你須要處理很大的圖像,這種任務會花費幾十秒鐘。 其次,當你但願擴展到多核應用的時候。 線程容許你的程序利用多核系統的優點。每個線程均可以被操做系統獨立調度,若是你的程序運行在多核機器上,調度器極可能會將每個線程分配到各自的處理器上面運行。 舉個例子,一個程序須要爲不少圖像生成縮略圖。一個具備固定 n 個線程的線程池,每個線程交給系統中的一個可用的 CPU 進行處理(咱們可使用QThread::idealThreadCount()獲取可用的 CPU 數)。這樣的調度將會把圖像縮放工做交給全部線程執行,從而有效地提高效率,幾乎達到與 CPU 數的線性提高(實際狀況不會這麼簡單,由於有時候 CPU 並非瓶頸所在)。 第三,當你不想被別人阻塞的時候。 這是一個至關高級的話題,因此你如今能夠暫時不看這段。這個問題的一個很好的例子是在 WebKit 中使用QNetworkAccessManager。WebKit 是一個現代的瀏覽器引擎。它幫助咱們展現網頁。Qt 中的QWebView就是使用的 WebKit。 QNetworkAccessManager則是 Qt 處理 HTTP 請求和響應的通用類。咱們能夠將它看作瀏覽器的網絡引擎。在 Qt 4.8 以前,這個類沒有使用任何協助工做線程,全部的網絡處理都是在QNetworkAccessManager及其QNetworkReply所在線程完成。 雖然在網絡處理中不使用線程是一個好主意,但它也有一個很大的缺點:若是你不能及時從 socket 讀取數據,內核緩衝區將會被填滿,因而開始丟包,傳輸速度將會直線降低。 socket 活動(也就是從一個 socket 讀取一些可用的數據)是由 Qt 的事件循環管理的。所以,阻塞事件循環將會致使傳輸性能的損失,由於沒有人會得到有數據可讀的通知,所以也就沒有人可以讀取這些數據。 可是什麼會阻塞事件循環?最壞的答案是:WebKit 本身!只要收到數據,WebKit 就開始生成網頁佈局。不幸的是,這個佈局的過程很是複雜和耗時,所以它會阻塞事件循環。儘管阻塞時間很短,可是足以影響到正常的數據傳輸(寬帶鏈接在這裏發揮了做用,在很短期內就能夠塞滿內核緩衝區)。 總結一下上面所說的內容: WebKit 發起一次請求 從服務器響應獲取一些數據 WebKit 利用到達的數據開始進行網頁佈局,阻塞事件循環 因爲事件循環被阻塞,也就沒有了可用的事件循環,因而操做系統接收了到達的數據,可是卻不能從QNetworkAccessManager的 socket 讀取 內核緩衝區被填滿,傳輸速度變慢 網頁的總體加載時間被自身的傳輸速度的下降而變得愈來愈壞。 注意,因爲QNetworkAccessManager和QNetworkReply都是QObject,因此它們都不是線程安全的,所以你不能將它們移動到另外的線程繼續使用。由於它們可能同時有兩個線程訪問:你本身的和它們所在的線程,這是由於派發給它們的事件會由後面一個線程的事件循環發出,但你不能肯定哪一線程是「後面一個」。 Qt 4.8 以後,QNetworkAccessManager默認會在一個獨立的線程處理 HTTP 請求,因此致使 GUI 失去響應以及操做系統緩衝區過快填滿的問題應該已經被解決了。 那麼,什麼狀況下不該該使用線程呢? 定時器 這多是最容易誤用線程的狀況了。若是咱們須要每隔一段時間調用一個函數,不少人可能會這麼寫代碼:
1
2
3
4
5
6
|
// 最錯誤的代碼
while
(condition)
{
doWork();
sleep(1);
// C 庫裏面的 sleep(3) 函數
}
|
當讀過咱們前面的文章以後,可能又會引入線程,改爲這樣的代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// 錯誤的代碼
class
Thread:
public
QThread
{
protected
:
voidrun()
{
while
(condition)
{
// 注意,若是咱們要在別的線程修改 condition,那麼它也須要加鎖
doWork();
sleep(1);
// 此次是 QThread::sleep()
}
}
};
|
最好最簡單的實現是使用定時器,好比QTimer,設置 1s 超時,而後將doWork()做爲槽:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class
Worker:
public
QObject
{
Q_OBJECT
public
:
Worker()
{
connect(&timer,SIGNAL(timeout()),
this
,SLOT(doWork()));
timer.start(1000);
}
privateslots:
void
doWork()
{
/* ... */
}
private
:
QTimer timer;
};
|
咱們所須要的就是開始事件循環,而後每隔一秒doWork()就會被自動調用。 網絡/狀態機 下面是一個很常見的處理網絡操做的設計模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
socket->connect(host);
socket->waitForConnected();
data=getData();
socket->write(data);
socket->waitForBytesWritten();
socket->waitForReadyRead();
socket->read(response);
reply=process(response);
socket->write(reply);
socket->waitForBytesWritten();
/* ... */
|
在通過前面幾章的介紹以後,不用多說,咱們就會發現這裏的問題:大量的waitFor*()函數會阻塞事件循環,凍結 UI 界面等等。注意,上面的代碼尚未加入異常處理,不然的話確定會更復雜。這段代碼的錯誤在於,咱們的網絡實際是異步的,若是咱們非得按照同步方式處理,就像拿起槍打本身的腳。爲了解決這個問題,不少人會簡單地將這段代碼移動到一個新的線程。 一個更抽象的例子是:
1
2
3
4
5
6
7
8
9
10
11
12
|
result=process_one_thing();
if
(result->something()){
process_this();
}
else
{
process_that();
}
wait_for_user_input();
input=read_user_input();
process_user_input(input);
/* ... */
|
這段抽象的代碼與前面網絡的例子有「殊途同歸之妙」。 讓咱們回過頭來看看這段代碼到底是作了什麼:咱們實際是想建立一個狀態機,這個狀態機要根據用戶的輸入做出合理的響應。例如咱們網絡的例子,咱們實際是想要構建這樣的東西:
空閒→正在鏈接(調用
connectToHost()
) 正在鏈接→成功鏈接(發出
connected()
信號)
成功鏈接→發送登陸數據(將登陸數據發送到服務器) 發送登陸數據→登陸成功(服務器返回ACK) 發送登陸數據→登陸失敗(服務器返回NACK) 以此類推。 既然知道咱們的實際目的,咱們就能夠修改代碼來建立一個真正的狀態機(Qt 甚至提供了一個狀態機類:QStateMachine)。建立狀態機最簡單的方法是使用一個枚舉來記住當前狀態。咱們能夠編寫以下代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
class
Object:
public
QObject
{
Q_OBJECT
enumState
{
State1,State2,State3
/* ... */
};
State state;
public
:
Object():state(State1)
{
connect(source,SIGNAL(ready()),
this
,SLOT(doWork()));
}
private
slots:
voiddoWork()
{
switch
(state)
{
caseState1:
/* ... */
state=State2;
break
;
caseState2:
/* ... */
state=State3;
break
;
/* ... */
}
}
};
|
source對象是哪來的?這個對象其實就是咱們關心的對象:例如,在網絡的例子中,咱們可能但願把 socket 的QAbstractSocket::connected()或者QIODevice::readyRead()信號與咱們的槽函數鏈接起來。固然,咱們很容易添加更多更合適的代碼(好比錯誤處理,使用QAbstractSocket::error()信號就能夠了)。這種代碼是真正異步、信號驅動的設計。 將任務分割成若干部分 假設咱們有一個很耗時的計算,咱們不能簡單地將它移動到另外的線程(或者是咱們根本沒法移動它,好比這個任務必須在 GUI 線程完成)。若是咱們將這個計算任務分割成小塊,那麼咱們就能夠及時返回事件循環,從而讓事件循環繼續派發事件,調用處理下一個小塊的函數。回一下如何實現隊列鏈接,咱們就能夠輕鬆完成這個任務:將事件提交到接收對象所在線程的事件循環;當事件發出時,響應函數就會被調用。 咱們可使用QMetaObject::invokeMethod()函數,經過指定Qt::QueuedConnection做爲調用類型來達到相同的效果。不過這要求函數必須是內省的,也就是說這個函數要麼是一個槽函數,要麼標記有Q_INVOKABLE宏。若是咱們還須要傳遞參數,咱們須要使用qRegisterMetaType()函數將參數註冊到 Qt 元類型系統。下面是代碼示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
class
Worker:
public
QObject
{
Q_OBJECT
public
slots:
void
startProcessing()
{
processItem(0);
}
void
processItem(intindex)
{
/* 處理 items[index] ... */
if
(index<numberOfItems)
{
QMetaObject::invokeMethod(
this
,
"processItem"
,
Qt::QueuedConnection,
Q_ARG(
int
,index+1));
}
}
};
|
因爲沒有任何線程調用,因此咱們能夠輕易對這種計算任務執行暫停/恢復/取消,以及獲取結果。 至此,咱們利用五個章節將有關線程的問題簡單介紹了下。線程應該說是所有設計裏面最複雜的部分之一,因此這部份內容也會比較困難。在實際運用中確定會更多的問題,這就只能讓咱們具體分析了。