最近在工作中用到了Qt中的多線程,踩了不少坑,故作下筆記角寸,警示后人 - -!
Overview
使用多線程編程可以最大限度地調用CPU資源忿墅,尤其對于多處理器系統(tǒng)扁藕。而對于界面開發(fā)而言,多線程一個十分重要的作用就是將復雜的運算處理分開執(zhí)行疚脐,以免造成界面的卡頓和凍結亿柑,甚至被系統(tǒng)強制關閉。
先來看看官方文檔是怎么教我們搞Qt的多線程的棍弄。首先望薄,可以建立一個Woker類和一個Controller類,想要放在線程中的后臺處理放在Worker類中呼畸,再使用QObject::moveToThread()與子線程關聯(lián)痕支,如下。
class Worker : public QObject {
Q_OBJECT
public slots:
void doWork(const QString ¶meter) {
QString result;
/* ... here is the expensive or blocking operation ... */
emit resultReady(result);
}
signals:
void resultReady(const QString &result);
};
class Controller : public QObject {
Q_OBJECT
QThread workerThread; //建立QThread對象
public:
Controller() {
Worker *worker = new Worker;
worker->moveToThread(&workerThread); //令worker和workerThread關聯(lián)
connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);
connect(this, &Controller::operate, worker, &Worker::doWork);
connect(worker, &Worker::resultReady, this, &Controller::handleResults);
workerThread.start();
}
~Controller() {
workerThread.quit();
workerThread.wait();
}
public slots:
void handleResults(const QString &);
signals:
void operate(const QString &);
};
可以看到役耕,通過信號槽機制采转,可以控制放在了子線程中的worker并互相通信。由于線程中的信號槽連接默認是隊列連接方式(稍后會講到)瞬痘,你可以安全地連接Worker對象與任意的Qobject對象。
Another way to make code run in a separate thread, is to subclass QThread and reimplement run()板熊。另一種方法就是繼承QThread并重寫run函數(shù)框全,如下
class WorkerThread : public QThread {
Q_OBJECT
void run() 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();
}
詳細的內容自己去看吧,具體用哪種方法看個人習慣了干签。好像乍眼一看也沒什么難的~然而
一號坑:子線程中操作UI
Qt創(chuàng)建的子線程中是不能對UI對象進行任何操作的津辩,即QWidget及其派生類對象,這個是我掉的第一個坑〈兀可能是由于考慮到安全性的問題闸度,所以Qt中子線程不能執(zhí)行任何關于界面的處理,包括消息框的彈出蚜印。正確的操作應該是通過信號槽莺禁,將一些參數(shù)傳遞給主線程,讓主線程(也就是Controller)去處理窄赋。
void Controller::handleResults(const int rslt, QString code,
QString id, QString desc, QString time)
{
lockThread->lck->lock_log << "controller handleResults start!\n";
lockThread->lck->lock_log.flush();
//(打開時)加密鎖有效
if (rslt == 1) {
lockThread->lck->lock_log << "controller handleResults 1!\n";
lockThread->lck->lock_log.flush();
m_isAuthorized = true;
QMessageBox::information(NULL, QStringLiteral("授權信息"),
QStringLiteral("授權成功!!"), QMessageBox::Yes);
}
//加密鎖失效
else if (rslt == 2) {
lockThread->lck->lock_log << "controller handleResults 2!\n";
lockThread->lck->lock_log.flush();
m_isAuthorized = false;
QMessageBox::information(NULL, QStringLiteral("授權信息"),
QStringLiteral("鎖ID:") + id + "\n" +
QStringLiteral("校驗碼:") + code, QMessageBox::Yes);
}
....
個人在VS2017環(huán)境下哟冬,在子線程中操作UI是直接崩潰的。
二號坑:信號的參數(shù)問題
這個就實屬有毒忆绰,搞了我好久浩峡。這個涉及到了Qt的元對象系統(tǒng)(Meta-Object System)和信號槽機制。
元對象系統(tǒng)即是提供了Qt類對象之間的信號槽機制的系統(tǒng)错敢。要使用信號槽機制翰灾,類必須繼承自QObject類,并在私有聲明區(qū)域聲明Q_OBJECT宏稚茅。當一個cpp文件中的類聲明帶有這個宏预侯,就會有一個叫moc工具的玩意創(chuàng)建另一個以moc開頭的cpp源文件(在debug目錄下),其中包含了為每一個類生成的元對象代碼峰锁。
在使用connect函數(shù)的時候萎馅,我們一般會把最后一個參數(shù)忽略掉。這時候我們需要看下函數(shù)原型
[static] QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal,
const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection)
可以看到虹蒋,最后一個參數(shù)代表的是連接的方式糜芳。
我們一般會用到方式是有三種
自動連接(AutoConnection),默認的連接方式魄衅。如果信號與槽峭竣,也就是發(fā)送者與接受者在同一線程,等同于直接連接晃虫;如果發(fā)送者與接受者處在不同線程皆撩,等同于隊列連接。
直接連接(DirectConnection)哲银。當信號發(fā)射時扛吞,槽函數(shù)立即直接調用。無論槽函數(shù)所屬對象在哪個線程荆责,槽函數(shù)總在發(fā)送者所在線程執(zhí)行滥比。
隊列連接(QueuedConnection)。當控制權回到接受者所在線程的事件循環(huán)時做院,槽函數(shù)被調用盲泛。這時候需要將信號的參數(shù)塞到信號隊列里濒持。槽函數(shù)在接受者所在線程執(zhí)行。
所以在線程間進行信號槽連接時寺滚,使用的是隊列連接方式柑营。在項目中,我定義的信號和槽的參數(shù)是這樣的
signals:
//自定義發(fā)送的信號
void myThreadSignal(const int, string, string, string, string);
貌似沒什么問題村视,然而實際運行起來槽函數(shù)根本就沒有被調用官套,程序沒有崩潰,VS也沒報錯蓖议。在查閱了N多博客和資料中才發(fā)現(xiàn)虏杰,在線程間進行信號槽連接時,參數(shù)不能隨便寫勒虾。
為什么呢纺阔?我的后四個參數(shù)是標準庫中的string類型,這不是元對象系統(tǒng)內置的類型修然,也不是c++的基本類型笛钝,系統(tǒng)無法識別,然后就沒有進入信號槽隊列中了愕宋,自然就會出現(xiàn)問題玻靡。解決方法有三種,最簡單的就是使用Qt的數(shù)據(jù)類型了
signals:
//自定義發(fā)送的信號
void myThreadSignal(const int, QString, QString, QString, QString);
第二種方法就是往元對象系統(tǒng)里注冊這個類型中贝。注意囤捻,在qRegisterMetaType函數(shù)被調用時,這個類型應該要確保已經(jīng)被完好地定義了邻寿。
qRegisterMetaType<MyClass>("MyClass");
方法三是改變信號槽的連接方式蝎土,將默認的隊列連接方式改為直接連接方式,這樣的話信號的參數(shù)直接進入槽函數(shù)中被使用绣否,槽函數(shù)立刻調用誊涯,不會進入信號槽隊列中。但這種方式官方認為有風險蒜撮,不建議使用暴构。
connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::DirectConnection)
至此,這個坑應該是能爬出來了段磨。
其它需要注意的問題
還有幾點需要注意
一定要用信號槽機制取逾,別想著直接調用,你會發(fā)現(xiàn)并沒有在子線程中執(zhí)行薇溃。
自定義的類不能指定父對象菌赖,因為moveToThread函數(shù)會將線程對象指定為自定義的類的父對象,當自定義的類對象已經(jīng)有了父對象沐序,就會報錯琉用。
當一個變量需要在多個線程間進行訪問時,最好加上voliate關鍵字策幼,以免讀取到的是舊的值邑时。當然,Qt中提供了線程同步的支持特姐,比如互斥鎖之類的玩意晶丘,使用這些方式來訪問變量會更加安全。
C++11標準中直接可以使用標準庫中提供的多線程類唐含,但個人覺得還是Qt好用
- Qt也提供了并發(fā)編程的支持浅浮,雖然這些玩意一般在服務器編程和高并發(fā)場景下用的比較多,界面編程比較少用捷枯。
至此總結完畢滚秩,希望您爬坑愉快 ( :
Reference:
Qt多線程間信號槽傳遞非QObject類型對象的參數(shù)
QT子線程與主線程的信號槽通信
Qt Creator快速入門_第三版 ——霍亞飛
Qt官方文檔