Qt多线程中的信号与槽
文章目錄
- 1 多線程中的信號與槽
- 2 對象的依附性
- 2.1 對象的依附性
- 2.2 開啟線程事件循環
- 2.3 線程事件循環的結束
- 2.4 設計實例
- 3 信號與槽的連接方式
- 3.1 Qt::DirectConnection(立即調用)
- 3.2 Qt::QueuedConnection(異步調用)
- 3.3 Qt::BlockingQueuedConnection(同步調用)
- 3.4 Qt::AutoConnection(默認連接)
- 3.5 Qt::UniqueConnection(單一連接)
1 多線程中的信號與槽
值得思考的問題:
- 線程對象是否可以發射信號(signal)?
- 是否可以定義槽函數(slot)?
QThread類擁有發射信號和定義槽函數的能力,關鍵的信號如下:
- void started():
- 線程開始運行時發射該信號。
- void finished():
- 線程完成運行時發射該信號。
- void terminated():
- 線程被異常終止時發射該信號。
讓人逃避的問題:
- 如果程序中有多個線程,槽函數是在哪個線程中執行的?
概念小科普:
- 進程中存在棧空間的概念(區別于棧數據結構)。
- 棧空間專用于函數調用(保存函數參數、局部變量等)。
- 線程擁有獨立的棧空間(可調用其它函數)。
小結論:
- 只要函數體中沒有訪問臨界資源的代碼,同一個函數可以被多個線程同時調用,且不會產生任何副作用!
實驗前的準備:
- 操作系統通過整型標識管理進程和線程:
- 進程擁有全局唯一的ID值(PID)。
- 線程有進程內唯一的ID值(TID)。
- QThread中的關鍵靜態成員函數:
- QThread* currentThread()。
- Qt::HANDLE currentThreadId()。
編程實驗:槽函數的運行上下文
MyThread.h:
#ifndef MYTHREAD_H #define MYTHREAD_H#include <QThread>class MyThread : public QThread {Q_OBJECT public:explicit MyThread(QObject *parent = 0);signals:void testSignal(); public slots:void onTestSignal();protected:void run(); };#endif // MYTHREAD_HMyThread.cpp:
#include "MyThread.h" #include <QDebug>MyThread::MyThread(QObject *parent) :QThread(parent) {connect(this, SIGNAL(testSignal()), this, SLOT(onTestSignal())); }void MyThread::run() {for (int i=0; i<10; i++){qDebug() << objectName() << " : " << i << " " << QThread::currentThreadId();msleep(200);}emit testSignal(); }void MyThread::onTestSignal() {qDebug() << "onTestSignal() : " << QThread::currentThreadId(); }MyObject.h:
#ifndef MYOBJECT_H #define MYOBJECT_H#include <QObject>class MyObject : public QObject {Q_OBJECT public:explicit MyObject(QObject *parent = 0);signals:public slots:void onStarted();void onFinished();void onTerminated();};#endif // MYOBJECT_HMyObject.cpp:
#include "MyObject.h" #include <QThread> #include <QDebug>MyObject::MyObject(QObject *parent) :QObject(parent) { }void MyObject::onStarted() {qDebug() << "onStarted() : " << QThread::currentThreadId(); }void MyObject::onFinished() {qDebug() << "onFinished() : " << QThread::currentThreadId(); }void MyObject::onTerminated() {qDebug() << "onFinished() : " << QThread::currentThreadId(); }main.cpp:
#include <QtCore/QCoreApplication> #include <QThread> #include <QDebug> #include "MyThread.h" #include "MyObject.h"int main(int argc, char *argv[]) {QCoreApplication a(argc, argv);qDebug() << QThread::currentThreadId();MyObject o;MyThread t;t.setObjectName("t");QObject::connect(&t, SIGNAL(started()), &o, SLOT(onStarted()));QObject::connect(&t, SIGNAL(finished()), &o, SLOT(onFinished()));QObject::connect(&t, SIGNAL(terminated()), &o, SLOT(onTerminated()));t.start();return a.exec(); }運行結果如下:
令人不解的問題:
- 當槽函數是線程中類的成員時,為什么依然不在本線程內被調用執行?
2 對象的依附性
2.1 對象的依附性
為了解決上述問題,我們需要弄清楚如下三個隱藏的問題:
- 對象依附于哪一個線程?
- 對象的依附性與槽函數執行的關系?
- 對象的依附性是否可以改變?
對象依附于哪一個線程?
- 默認情況下,對象依附于自身被創建的線程;例如:對象在主線程(main()函數)中被創建,則依附于主線程。
對象的依附性與槽函數執行的關系?
- 默認情況下,槽函數在其所依賴的線程中被調用執行!
對象的依附性是否可以改變?
- QObject::moveToThread用于改變對象的線程依附性,使得對象的槽函數在依附的線程中被調用執行。
我們將代碼進行簡單修改,main函數如下:
TestThread.cpp的內容如下:
執行結果如下:
出現了一個很奇怪的問題:
- 實驗中對象m的槽函數為什么沒有被調用執行?
2.2 開啟線程事件循環
對此,我們需要了解下線程中的事件循環:
- 信號與槽的機制需要事件循環的支持。
- QThread類中提供的exec()函數用于開啟線程的事件循環。
- 只有事件循環開啟,槽函數才能在信號發送后被調用。
小結論:
- 前提條件:
- 對象依附的線程開啟了事件循環。
- 后置結果:
- 對象中的槽函數在依附的線程中被調用執行。
我們將TestThread.cpp的內容進行簡單的修改:
執行結果如下:
再來進一步實驗,如果我們將t的依附線程更改為自己會發生什么呢?
對main函數進行如下修改:
輸出結果如下:
我們可以看到槽函數在信號發送時立刻執行,而不是在開啟事件循環后才執行!
我們再來看一下研究槽函數的具體執行線程有什么意義?
- 當信號的發送與對應槽函數的執行在不同線程中時,可能產生臨界資源的競爭問題!
2.3 線程事件循環的結束
有趣的問題:
- 如果線程體函數中開啟了事件循環,線程如何正常結束呢?
QThread::exec()使得線程進入事件循環:
- 事件循環結束前,exec()后的語句無法執行。
- quit()和exit()函數用于結束事件循環。
- quit()相當于exit(0),exec()的返回值由exit()參數決定。
注意:
- 無論事件循環是否開啟,信號發送后會直接進入對象所依附線程的事件隊列;然而,只有開啟了事件循環,對應的槽函數才會在線程中被調用。
main函數修改為如下即可:
2.4 設計實例
設計相關的問題:
- 什么時候需要在線程中開啟事件循環?
設計原則:
- 事務性操作(間斷性IO操作等)可以開啟線程的事件循環;每次操作通過發送信號的方式使得槽函數在子線程中執行。
文件緩沖區:
- 默認情況下,文件操作時會開辟一段內存作為緩沖區。
- 向文件中寫入的數據會先進入緩沖區。
- 只有當緩沖區滿或者遇見換行符才將數據寫入磁盤。
- 緩沖區的意義在于,減少磁盤的低級IO操作,提高文件讀寫效率!
編程實驗:文件操作示例
FileWriter.h:
#ifndef FILEWRITER_H #define FILEWRITER_H#include <QObject> #include <QFile> #include <QThread>class FileWriter : public QObject {Q_OBJECTclass Worker : public QThread{protected:void run();};QFile m_file;Worker m_worker; public:explicit FileWriter(QString file, QObject *parent = 0);bool open();void write(QString text);void close();~FileWriter(); signals:void doWrite(QString text);void doClose(); protected slots:void writeSlot(QString text);void closeSlot(); };#endif // FILEWRITER_HFileWriter.cpp:
#include "FileWriter.h" #include <QDebug>void FileWriter::Worker::run() {qDebug() << "void FileWriter::Worker::run() - begin: tid = " << currentThreadId();exec();qDebug() << "void FileWriter::Worker::run() - end"; }FileWriter::FileWriter(QString file, QObject *parent) :QObject(parent), m_file(file) {connect(this, SIGNAL(doWrite(QString)), this, SLOT(writeSlot(QString)));connect(this, SIGNAL(doClose()), this, SLOT(closeSlot()));moveToThread(&m_worker);m_worker.start(); }bool FileWriter::open() {return m_file.open(QIODevice::WriteOnly | QIODevice::Text); }void FileWriter::write(QString text) {qDebug() << "void FileWriter::write(QString text) tid = " << QThread::currentThreadId();emit doWrite(text); }void FileWriter::close() {qDebug() << "void FileWriter::close() tid = " << QThread::currentThreadId();emit doClose(); }void FileWriter::writeSlot(QString text) {qDebug() << "void FileWriter::writeSlot(QString text) tid = " << QThread::currentThreadId();m_file.write(text.toAscii());m_file.flush(); }void FileWriter::closeSlot() {qDebug() << "void FileWriter::closeSlot() tid = " << QThread::currentThreadId();m_file.close(); }FileWriter::~FileWriter() {m_worker.quit(); }main.cpp:
#include <QtCore/QCoreApplication> #include <QDebug> #include <QThread> #include "FileWriter.h"int main(int argc, char *argv[]) {QCoreApplication a(argc, argv);qDebug() << "main() tid = " << QThread::currentThreadId();FileWriter writer("C:/Users/hp/Desktop/test.txt");if( writer.open() ){writer.write("D.T.Software\r\n");writer.write("中文測試\r\n");writer.write("狄泰軟件\r\n");writer.close();}return a.exec(); }Qt線程的使用模式:
- 無事件循環模式:
- 后臺執行長時間的耗時任務:文件復制、網絡數據讀取等。
- 開啟事件循環模式:
- 執行事務性操作:文件寫入、數據庫寫入等。
總結一下:
- 事務性操作可以開啟線程的事件循環,將操作分攤到子線程。
- 工程開發中,多數情況不會開啟線程的事件循環。
- 線程多用于執行后臺任務或者耗時任務。
3 信號與槽的連接方式
深入信號與槽的連接方式:
小知識:
知識回顧:
3.1 Qt::DirectConnection(立即調用)
直接在發送信號的線程中調用槽函數,等價于槽函數的實時調用!
void direct_connection() {static TestThread t;static MyObject m;QObject::connect(&t, SIGNAL(testSignal()), &m, SLOT(testSlot()), Qt::DirectConnection);t.start();t.wait(5 * 1000);t.quit(); }輸入結果:
3.2 Qt::QueuedConnection(異步調用)
信號發送至目標線程的事件隊列,由目標線程處理,當前線程繼續向下執行!
結果:
3.3 Qt::BlockingQueuedConnection(同步調用)
信號發送至目標線程的事件隊列,由目標線程處理,當前線程等待槽函數返回,之后繼續向下執行!
注意:目標線程和當前線程必須不同!
void blocking_queued_connection() {static TestThread t;static MyObject m;QObject::connect(&t, SIGNAL(testSignal()), &m, SLOT(testSlot()), Qt::BlockingQueuedConnection);t.start();t.wait(5 * 1000);t.quit(); }輸出結果:
這里的輸出結果值得分析下:
- 我們可以看到雖然執行了線程的quit函數但是線程并沒有退出事件循環,而是等待槽函數執行完畢才退出事件循環。
3.4 Qt::AutoConnection(默認連接)
圖中略有錯誤,只有當發送線程依附的線程等于接收線程時才相當于立即調用,否則還是異步調用,會放到事件隊列中去。
輸出結果:
3.5 Qt::UniqueConnection(單一連接)
描述:
- 功能與AutoConnection相同,自動確定連接類型。
- 同一個信號與同一個參函數之間只有一個連接。
小知識:
- 默認情況下,同一個信號可以多次連接到同一個槽函數。
- 多次連接意味著同一個槽函數的多次調用。
輸出結果:
參考資料:
總結
以上是生活随笔為你收集整理的Qt多线程中的信号与槽的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 银行家算法的分析与实现
- 下一篇: 名下没有房子 可以迁户口吗?