#(74):線程和 QObject
前面兩個章節我們從事件循環和線程類庫兩個角度闡述有關線程的問題。本章我們將深入線程間得交互,探討線程和`QObject`之間的關系。在某種程度上,這才是多線程編程真正需要注意的問題。
現在我們已經討論過事件循環。我們說,每一個 Qt 應用程序至少有一個事件循環,就是調用了`QCoreApplication::exec()`的那個事件循環。不過,`QThread`也可以開啟事件循環。只不過這是一個受限于線程內部的事件循環。因此我們將處于調用`main()`函數的那個線程,并且由`QCoreApplication::exec()`創建開啟的那個事件循環成為主事件循環,或者直接叫主循環。注意,`QCoreApplication::exec()`只能在調用`main()`函數的線程調用。主循環所在的線程就是主線程,也被成為 GUI 線程,因為所有有關 GUI 的操作都必須在這個線程進行。`QThread`的局部事件循環則可以通過在`QThread::run()`中調用`QThread::exec()`開啟:
~~~
class Thread : public QThread
{
protected:
void run() {
/* ... 初始化 ... */
exec();
}
};
~~~
記得我們前面介紹過,Qt 4.4 版本以后,`QThread::run()`不再是純虛函數,它會調用`QThread::exec()`函數。與`QCoreApplication`一樣,`QThread`也有`QThread::quit()`和`QThread::exit()`函數來終止事件循環。
線程的事件循環用于為線程中的所有`QObjects`對象分發事件;默認情況下,這些對象包括線程中創建的所有對象,或者是在別處創建完成后被移動到該線程的對象(我們會在后面詳細介紹“移動”這個問題)。我們說,一個`QObject`的所依附的線程(thread affinity)是指它所在的那個線程。它同樣適用于在`QThread`的構造函數中構建的對象:
~~~
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`對象。
[](http://files.devbean.net/images/2013/12/threadsandobjects.png)
我們可以使用線程安全的`QCoreApplication::postEvent()`函數向一個對象發送事件。它將把事件加入到對象所在的線程的事件隊列中,因此,如果這個線程沒有運行事件循環,這個事件也不會被派發。
值得注意的一點是,`QObject`及其所有子類都不是線程安全的(但都是可重入的)。因此,你不能有兩個線程同時訪問一個`QObject`對象,除非這個對象的內部數據都已經很好地序列化(例如為每個數據訪問加鎖)。記住,在你從另外的線程訪問一個對象時,它可能正在處理所在線程的事件循環派發的事件!基于同樣的原因,你也不能在另外的線程直接`delete`一個`QObject`對象,相反,你需要調用`QObject::deleteLater()`函數,這個函數會給對象所在線程發送一個刪除的事件。
此外,`QWidget`及其子類,以及所有其它 GUI 相關類(即便不是`QObject`的子類,例如`QPixmap`),甚至不是可重入的:它們只能在 GUI 線程訪問。
`QObject`的線程依附性是可以改變的,方法是調用`QObject::moveToThread()`函數。該函數會改變一個對象及其所有子對象的線程依附性。由于`QObject`不是線程安全的,所以我們只能在該對象所在線程上調用這個函數。也就是說,我們只能在對象所在線程將這個對象移動到另外的線程,不能在另外的線程改變對象的線程依附性。還有一點是,Qt 要求`QObject`的所有子對象都必須和其父對象在同一線程。這意味著:
* 不能對有父對象(parent 屬性)的對象使用`QObject::moveToThread()`函數
* 不能在`QThread`中以這個`QThread`本身作為父對象創建對象,例如:
~~~
class Thread : public QThread {
void run() {
QObject *obj = new QObject(this); // 錯誤!
}
};
~~~
這是因為`QThread`對象所依附的線程是創建它的那個線程,而不是它所代表的線程。
Qt 還要求,在代表一個線程的`QThread`對象銷毀之前,所有在這個線程中的對象都必須先`delete`。要達到這一點并不困難:我們只需在`QThread::run()`的棧上創建對象即可。
現在的問題是,既然線程創建的對象都只能在函數棧上,怎么能讓這些對象與其它線程的對象通信呢?Qt 提供了一個優雅清晰的解決方案:我們在線程的事件隊列中加入一個事件,然后在事件處理函數中調用我們所關心的函數。顯然這需要線程有一個事件循環。這種機制依賴于 moc 提供的反射:因此,只有信號、槽和使用`Q_INVOKABLE`宏標記的函數可以在另外的線程中調用。
`QMetaObject::invokeMethod()`靜態函數會這樣調用:
~~~
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 檢查的是**信號發出的線程**,而不是信號發出的對象所在的線程!我們可以看看下面的代碼:
~~~
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();
~~~
這里的`obj`發出`aSignal()`信號時,使用哪種連接方式?答案是:直接連接。因為`Thread`對象所在線程發出了信號,也就是信號發出的線程與接受者是同一個。在`aSlot()`槽函數中,我們可以直接訪問`Thread`的某些成員變量,但是注意,在我們訪問這些成員變量時,`Thread::run()`函數可能也在訪問!這意味著二者并發進行:這是一個完美的導致崩潰的隱藏bug。
另外一個例子可能更為重要:
class Thread : public QThread
{
Q_OBJECT
slots:
void aSlot() {
/* ... */
}
protected:
void run() {
QObject *obj = new Object;
connect(obj, SIGNAL(aSignal()), this, SLOT(aSlot()));
/* ... */
}
};
這個例子也會使用隊列連接。然而,這個例子比上面的例子更具隱蔽性:在這個例子中,你可能會覺得,`Object`所在`Thread`所代表的線程中被創建,又是訪問的`Thread`自己的成員數據。稍有不慎便會寫出這種代碼。
為了解決這個問題,我們可以這么做:`Thread`構造函數中增加一個函數調用:`moveToThread(this)`:
~~~
class Thread : public QThread {
Q_OBJECT
public:
Thread() {
moveToThread(this); // 錯誤!
}
/* ... */
};
~~~
實際上,這的確可行(因為`Thread`的線程依附性被改變了:它所在的線程成了自己),但是這并不是一個好主意。這種代碼意味著我們其實誤解了線程對象(`QThread`子類)的設計意圖:`QThread`對象不是線程本身,它們其實是用于管理它所代表的線程的對象。因此,它們應該在另外的線程被使用(通常就是它自己所在的線程),而不是在自己所代表的線程中。
上面問題的最好的解決方案是,將處理任務的部分與管理線程的部分分離。簡單來說,我們可以利用一個`QObject`的子類,使用`QObject::moveToThread()`改變其線程依附性:
~~~
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();
~~~
- (1)序
- (2)Qt 簡介
- (3)Hello, world!
- (4)信號槽
- (5)自定義信號槽
- (6)Qt 模塊簡介
- (7)MainWindow 簡介
- (8)添加動作
- (9)資源文件
- (10)對象模型
- (11)布局管理器
- (12)菜單欄、工具欄和狀態欄
- (13)對話框簡介
- (14)對話框數據傳遞
- (15)標準對話框 QMessageBox
- (16)深入 Qt5 信號槽新語法
- (17)文件對話框
- (18)事件
- (19)事件的接受與忽略
- (21)事件過濾器
- (22)事件總結
- (23)自定義事件
- (24)Qt 繪制系統簡介
- (25)畫刷和畫筆
- (26)反走樣
- (27)漸變
- (28)坐標系統
- (29)繪制設備
- (30)Graphics View Framework
- (31)貪吃蛇游戲(1)
- (32)貪吃蛇游戲(2)
- (33)貪吃蛇游戲(3)
- (34)貪吃蛇游戲(4)
- (35)文件
- (36)二進制文件讀寫
- (37)文本文件讀寫
- (38)存儲容器
- (39)遍歷容器
- (40)隱式數據共享
- (41)model/view 架構
- (42)QListWidget、QTreeWidget 和 QTableWidget
- (43)QStringListModel
- (44)QFileSystemModel
- (45)模型
- (46)視圖和委托
- (47)視圖選擇
- (48)QSortFilterProxyModel
- (49)自定義只讀模型
- (50)自定義可編輯模型
- (51)布爾表達式樹模型
- (52)使用拖放
- (53)自定義拖放數據
- (54)剪貼板
- (55)數據庫操作
- (56)使用模型操作數據庫
- (57)可視化顯示數據庫數據
- (58)編輯數據庫外鍵
- (59)使用流處理 XML
- (60)使用 DOM 處理 XML
- (61)使用 SAX 處理 XML
- (62)保存 XML
- (63)使用 QJson 處理 JSON
- (64)使用 QJsonDocument 處理 JSON
- (65)訪問網絡(1)
- (66)訪問網絡(2)
- (67)訪問網絡(3)
- (68)訪問網絡(4)
- (69)進程
- (70)進程間通信
- (71)線程簡介
- (72)線程和事件循環
- (73)Qt 線程相關類
- (74)線程和 QObject
- (75)線程總結
- (76)QML 和 QtQuick 2
- (77)QML 語法
- (78)QML 基本元素
- (79)QML 組件
- (80)定位器
- (81)元素布局
- (82)輸入元素
- (83)Qt Quick Controls
- (84)Repeater
- (85)動態視圖
- (86)視圖代理
- (87)模型-視圖高級技術
- (88)Canvas
- (89)Canvas(續)