在開發存在介面互動的程式中,為了使一些耗時操作不造成卡頓;我們一般會將這些耗時操作放到子執行緒中進行處理,常見的如一些同步通訊。
雖然已編寫過幾次多執行緒的程式,但是每次使用都感覺心裡不踏實,借用 QThread
總結一下罷。
Qt 中使用多執行緒,必然繞不開的是 QThread
。建議先過一遍 QThread Class 檔案。
檔案中演示了兩種使用方法:
/*------------------------------WorkerThread-----------------------------------*/
class WorkerThread : public QThread
{
Q_OBJECT
public:
explicit WorkerThread();
protected:
void run();
signals:
void resultReady(const QString &s);
};
void WorkerThread::run(){
/* ... here is the expensive or blocking operation ... */
}
/*------------------------------MainWindow-----------------------------------*/
void MainWindow::startWorkInAThread()
{
WorkerThread *workerThread = new WorkerThread(this);
// Release object in workerThread
connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater);
workerThread->start();
}
需要 注意 的:
run()
中未呼叫 exec()
開啟 even loop
,那麼在 run()
執行結束時,執行緒將自動退出。
該案例中,WorkerThread
存在於範例化它的舊執行緒中,僅有 run()
中是在子執行緒中執行的。我們可以通過以下程式碼 列印執行緒ID
進行驗證:
qDebug()<<"mythread QThread::currentThreadId()==" << QThread::currentThreadId();
這就存在一個尷尬的問題,如果在 WorkerThread
的 run()
中使用了 WorkerThread
的成員變數,而且 QThread
的其他方法也使用到了它,即我們從不同執行緒存取該成員變數,這時需要自行檢查這樣是否安全。
這個例子也說明了,QThread
範例本身並不是一個執行緒,正如 QThread Class
開篇點明這是一個 執行緒管理類 。
The QThread class provides a platform-independent way to manage threads.
注意:這種使用方法並不推薦,至於它為什麼仍然出現在 QThread的檔案裡作為案例 「誤導」 我們,這貌似是個歷史問題。
在 Qt 4.4
版本以前的 QThread
類是個抽象類,要想編寫多執行緒程式碼唯一的做法就是 繼承 QThread
類。該論斷在 Qt4.3
的 QThread Class 中可以印證。
但是之後的版本中,Qt
庫完善了執行緒的親和性以及訊號槽機制,我們有了更為優雅的使用執行緒的方式,即 QObject::moveToThread()
。但是即使在 2020
的今天,網上仍然有不少教學教我們使用Qt多執行緒的舊方法;難怪 Bradley T. Hughes
在 2010
專門寫了篇 You’re doing it wrong…,為此我只能表示:
下面介紹推薦做法。
/*--------------------DisconnectMonitor-------------------------*/
class DisconnectMonitor : public QObject
{
Q_OBJECT
public:
explicit DisconnectMonitor();
signals:
void StartMonitor(long long hanlde);
void StopMonitor();
// if Controller disconnect emit this signal
void Disconnect();
private slots:
void slot_StartMonitor(long long hanlde);
void slot_StopMonitor();
// State machine
void Monitor();
private:
long long ControllerHanlde;
QTimer *MonitorTimer;
};
DisconnectMonitor::DisconnectMonitor()
{
// New a Timer monitor controller by timing
MonitorTimer = new QTimer;
ControllerHanlde = 0;
connect(MonitorTimer,&QTimer::timeout,this,&DisconnectMonitor::Monitor);
connect(this,&DisconnectMonitor::StartMonitor,this,&DisconnectMonitor::slot_StartMonitor);
connect(this,&DisconnectMonitor::StopMonitor,this,&DisconnectMonitor::slot_StopMonitor);
MonitorTimer->start(TAKETIME);
}
void DisconnectMonitor::Monitor(){
// if not Controller -> return
if(0 == ControllerHanlde){
return;
}
//else Listening
else{
int state = IsConnect(ControllerHanlde);
if (0 != state){
emit Disconnect();
}
}
}
/*---------------------------Controller----------------------------*/
class Controller : public QObject
{
Q_OBJECT
QThread workerThread;
public:
Controller() {
DisconnectMonitor *monitor = new DisconnectMonitor;
monitor->moveToThread(&workerThread);
connect(workerThread, &QThread::finished, monitor, &QObject::deleteLater);
connect(monitor,SIGNAL(Disconnect()),this,SLOT(DisconnectManage()));
workerThread.start();
}
~Controller() {
workerThread.quit();
workerThread.wait();
}
private slots:
void DisconnectManage();
};
這裡通過 moveToThread()
將 Object
物件移到到新執行緒中,如此一來整個 monitor
都將在子執行緒中執行(其實這句話是有問題的,這是一個感性的理解)。
我們在 DisconnectMonitor
中定義了一個定時器用以實現定時檢測。由於不能跨執行緒操作DisconnectMonitor
中的定時器,我們在類建立時就開啟定時器,在超時事件中實現定時監聽,如果檢測到裝置斷開了,就傳送 Disconnect()
訊號。
使用 movetoThread()
需要注意的是:
上面的案例中,並不能認為 monitor 的控制權歸屬於新執行緒!它仍然屬於主執行緒,正如一位博主所說【在哪裡建立就屬於哪裡】。movetoThread()的作用是將槽函數在指定的執行緒中呼叫。僅有槽函數在指定執行緒中呼叫,包括建構函式都仍然在主執行緒中呼叫!!!
DisconnectMonitor
須繼承自 頂層 父類別 Object
,否則不能移動。
如果 Thread
為 nullptr
,則該物件及其子物件的所有事件處理都將停止,因為它們不再與任何執行緒關聯。
呼叫 movetoThread()
時,移動物件的所有計時器將被重置。 計時器首先在當前執行緒中停止,然後在targetThread中重新啟動(以相同的間隔),這時定時器屬於子執行緒。若線上程之間不斷移動物件可能會無限期地延遲計時器事件。
QObject Class
中 特別提醒:movetoThread()
是執行緒不安全的,它只能見一個物件「推」到另一個執行緒,而不能將物件從任意執行緒推到當前執行緒,除非這個物件不再與任何執行緒關聯。
connect
函數原型如下:
static QMetaObject::Connection connect(const QObject *sender, const char *signal,
const QObject *receiver, const char *member, Qt::ConnectionType = Qt::AutoConnection);
static QMetaObject::Connection connect(const QObject *sender, const QMetaMethod &signal,
const QObject *receiver, const QMetaMethod &method,
Qt::ConnectionType type = Qt::AutoConnection);
inline QMetaObject::Connection connect(const QObject *sender, const char *signal,
const char *member, Qt::ConnectionType type = Qt::AutoConnection) const;
我們經常使用 connect
,但是確很少留意最後一個引數 Qt::ConnectionType
Qt::AutoConnection
預設連線型別,如果訊號接收方與傳送方在同一個執行緒,則使用 Qt::DirectConnection
,否則使用 Qt::QueuedConnection
;連線型別在訊號 發射時 決定。
Qt::DirectConnection
訊號所連線至的槽函數將會被立即執行,並且是在發射訊號的執行緒;倘若槽函數執行的是耗時操作、訊號由 UI執行緒
發射,則會 阻塞 Qt的事件迴圈,UI會進入 無響應狀態 。
Qt::QueuedConnection
槽函數將會在接收者的執行緒被執行,此種連線型別下的訊號倘若被多次觸發、相應的槽函數會在接收者的執行緒裡被順次執行相應次數;當使用 QueuedConnection
時,引數型別必須是Qt基本型別,或者使用 qRegisterMetaType()
進行註冊了的自定義型別。
Qt::BlockingQueuedConnection
和 Qt::QueuedConnection
類似,區別在於傳送訊號的執行緒在槽函數執行完畢之前一直處於阻塞狀態;收發雙方必須不在同一執行緒,否則會導致 死鎖 。
Qt::UniqueConnection
執行方式與 AutoConnection
相同,不過關聯是唯一的。(如果相同兩個物件,相同的訊號關聯到相同的槽,那麼第二次 connect
將失敗)
注意:
如果接受者執行緒中有一個事件迴圈,那麼當傳送者與接受者在不同的執行緒中時,使用 DirectConnection
是不安全的;類似的,呼叫其他執行緒中的物件的任何函數也是不安全的。值得留意的是,QObject::connect()
函數本身是執行緒安全的。
若使用預設的 run()
方法或自行呼叫 exec()
,則QThread將開啟事件迴圈。QThread
同樣提供了 exit()
函數和 quit()
槽。這賦予了QThread使用需要事件迴圈的非GUI類的能力(QTimer
、QTcpSocket
等)。也使得該執行緒可以關聯任意一個執行緒的訊號到指定執行緒的槽函數。如果一個執行緒沒有開啟事件迴圈,那麼該執行緒中的 timeout()
將永遠不會發射。
如果在一個執行緒中建立了OBject
物件,那麼發往這個物件的事件將由該執行緒的事件迴圈進行分派。
我們可以手動使用 QCoreApplication::postEvent()
在任何時間先任何物件傳送事件,該函數是執行緒安全的。
看到這,對執行緒的建立尚有困惑,於是查詢了一下 Qt 的原始碼。目前在 qthread_win.cpp
找到答案,至於程式是如何從 QThread -> qthread_win
尚不清楚。
使用時,我們均以 QThread->start()
開啟執行緒:
/*-----------------------qthread_win.cpp---------------------------------*/
void QThread::start(Priority priority)
{
Q_D(QThread);
QMutexLocker locker(&d->mutex);
if (d->isInFinish) {
locker.unlock();
wait();
locker.relock();
}
if (d->running)
return;
d->running = true;
d->finished = false;
d->exited = false;
d->returnCode = 0;
d->interruptionRequested = false;
/*
NOTE: we create the thread in the suspended state, set the
priority and then resume the thread.
since threads are created with normal priority by default, we
could get into a case where a thread (with priority less than
NormalPriority) tries to create a new thread (also with priority
less than NormalPriority), but the newly created thread preempts
its 'parent' and runs at normal priority.
*/
// 【1】判斷當前環境,呼叫系統API建立執行緒 d->handle 為執行緒控制程式碼
#if defined(Q_CC_MSVC) && !defined(_DLL) // && !defined(Q_OS_WINRT)
# ifdef Q_OS_WINRT
# error "Microsoft documentation says this combination leaks memory every time a thread is started. " \
"Please change your build back to -MD/-MDd or, if you understand this issue and want to continue, " \
"edit this source file."
# endif
// MSVC -MT or -MTd build
d->handle = (Qt::HANDLE) _beginthreadex(NULL, d->stackSize, QThreadPrivate::start,
this, CREATE_SUSPENDED, &(d->id));
#else
// MSVC -MD or -MDd or MinGW build
d->handle = CreateThread(nullptr, d->stackSize,
reinterpret_cast<LPTHREAD_START_ROUTINE>(QThreadPrivate::start),
this, CREATE_SUSPENDED, reinterpret_cast<LPDWORD>(&d->id));
#endif // Q_OS_WINRT
//建立執行緒失敗
if (!d->handle) {
qErrnoWarning("QThread::start: Failed to create thread");
d->running = false;
d->finished = true;
return;
}
//優先順序
int prio;
d->priority = priority;
switch (d->priority) {
case IdlePriority:
prio = THREAD_PRIORITY_IDLE;
break;
case LowestPriority:
prio = THREAD_PRIORITY_LOWEST;
break;
case LowPriority:
prio = THREAD_PRIORITY_BELOW_NORMAL;
break;
case NormalPriority:
prio = THREAD_PRIORITY_NORMAL;
break;
case HighPriority:
prio = THREAD_PRIORITY_ABOVE_NORMAL;
break;
case HighestPriority:
prio = THREAD_PRIORITY_HIGHEST;
break;
case TimeCriticalPriority:
prio = THREAD_PRIORITY_TIME_CRITICAL;
break;
case InheritPriority:
default:
prio = GetThreadPriority(GetCurrentThread());
break;
}
if (!SetThreadPriority(d->handle, prio)) {
qErrnoWarning("QThread::start: Failed to set thread priority");
}
if (ResumeThread(d->handle) == (DWORD) -1) {
qErrnoWarning("QThread::start: Failed to resume new thread");
}
}
核心為【1】我們先找找 _beginthreadex
原型,這是一個 Windows
系統 API
:
unsigned long _beginthreadex(
void *security, // 安全屬性,NULL為預設安全屬性
unsigned stack_size, // 指定執行緒堆疊的大小。如果為0,則執行緒堆疊大小和建立它的執行緒的相同。一般用0
unsigned ( __stdcall *start_address )( void * ),
// 指定執行緒函數的地址,也就是執行緒呼叫執行的函數地址(用函數名稱即可,函數名稱就表示地址)
void *arglist, // 傳遞給執行緒的引數的指標,可以通過傳入物件的指標,線上程函數中再轉化為對應類的指標
unsigned initflag, // 執行緒初始狀態,0:立即執行;CREATE_SUSPEND:suspended(懸掛)
unsigned *thrdaddr // 用於記錄執行緒ID的地址
對應原始碼食用,可發現執行緒函數地址為 QThreadPrivate::start
,跟蹤一下:
unsigned int __stdcall QT_ENSURE_STACK_ALIGNED_FOR_SSE QThreadPrivate::start(void *arg) noexcept
{
// 強制轉換
QThread *thr = reinterpret_cast<QThread *>(arg);
QThreadData *data = QThreadData::get2(thr);
qt_create_tls();
TlsSetValue(qt_current_thread_data_tls_index, data);
data->threadId.storeRelaxed(reinterpret_cast<Qt::HANDLE>(quintptr(GetCurrentThreadId())));
QThread::setTerminationEnabled(false);
{
QMutexLocker locker(&thr->d_func()->mutex);
data->quitNow = thr->d_func()->exited;
}
data->ensureEventDispatcher();
#if !defined(QT_NO_DEBUG) && defined(Q_CC_MSVC) && !defined(Q_OS_WINRT)
// sets the name of the current thread.
QByteArray objectName = thr->objectName().toLocal8Bit();
qt_set_thread_name(HANDLE(-1),
objectName.isEmpty() ?
thr->metaObject()->className() : objectName.constData());
#endif
//發射 started 訊號
emit thr->started(QThread::QPrivateSignal());
QThread::setTerminationEnabled(true);
//呼叫QThread,run函數
thr->run();
finish(arg);
return 0;
}
可以發現呼叫了 QThread
的 run()
方法。
而該方法預設開啟事件迴圈:
void QThread::run()
{
(void) exec();
}
這樣我們的執行緒就跑起來了。
首先,刪除 QThread
物件並不會停止其管理的執行緒的執行。刪除正在執行的 QThread
將導致 程式奔潰。在刪除 QThread
之前我們需要等待 finish
訊號。
對於未開啟事件迴圈的執行緒,我們僅需讓 run()
執行結束即可終止執行緒,常見的做法是通過 bool
變數進行控制。由於我們的 bool runenanble
被多執行緒存取,這裡我們需要定義一個 QMutex
進行加鎖保護。至於加鎖的效率問題,網上有大佬測出大概速度會降低1.5倍(Release模式)。
void TestThread::stopThread(){
mutex.lock();
runenanble = false;
mutex.unlock();
}
void TestThread::run(){
runenanble = true;
while(1){
if(mutex.tryLock()){
if(!runenable)
break;
else{
/*dosomething*/
}
}
}
}
對於開啟了事件迴圈的執行緒,正常的退出執行緒其實質是退出事件迴圈。
quit()/exit() + wait()
若執行緒中開始開啟了 EvenLoop
,耗時程式碼執行結束後,執行緒並不會退出。我們可呼叫 quit()/exit() + wait()
實現退出。
terminate()+ wait()
呼叫 terminate()
後,將根據作業系統的排程,執行緒可能立即結束也可能不會,終止之後仍需使用 wait()
。
由於執行緒可能在任何位置終止,強制結束執行緒是危險的操作,可能在修改資料資料時終止,可能導致執行緒狀態無法清除,可能導致鎖異常。因此並 不推薦使用。
finished
僅依靠上面的方法退出執行緒,可能存在 記憶體漏失 的情況。注意到官方案例中都使用了finished
訊號:
connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater);
如果類物件儲存在 棧 上,自然銷燬由作業系統自動完成;如果是儲存在 堆 上,沒有父物件的指標要想正常銷燬,需要自行釋放。
從 Qt4.8
開始,我們就可以通過將 finished()
訊號連結至 Object::deleteLater()
來釋放剛剛結束的執行緒中的物件。
上文例二的 QThread
並未 new
出來,這樣在解構時就需要呼叫 Thread::wait()
,如果是堆分配的話, 可以通過 deleteLater
來讓執行緒自殺。
QThread workerThread = new QThread();
connect(workerThread, &QThread::finished, workerThread, &QObject::deleteLater);
注意:程式退出前,需要判斷各執行緒是否已退出,如果不進行判斷,很可能程式退出時會崩潰。如果執行緒的父物件是視窗物件,那麼在表單的解構函式中,還需要呼叫 wait()
等待執行緒完全結束再進行下面的解構。
大多數作業系統都為執行緒堆疊設定了最大和最小限制。如果超出這些限制,執行緒將無法啟動。
每個執行緒都有自己的棧,彼此獨立,由編譯器分配。一般在 Windows
的棧大小為 2M
,在 Linux
下是 8M
。
Qt 提供了獲取以及設定棧空間大小的函數:stackSize()
、setStackSize(uint stackSize)
。其中 stackSize()
函數不是返回當前所線上程的棧大小,而是獲取用 stackSize()
函數手動設定的棧大小。
沒錯,QThread
不再讓執行緒間拼得你死我活,我們可以通過 setPriority()
設定執行緒優先順序,通過 priority()
獲取執行緒優先順序。
Constant | Value | Description |
---|---|---|
QThread::IdlePriority | 0 | scheduled only when no other threads are running. |
QThread::LowestPriority | 1 | scheduled less often than LowPriority. |
QThread::LowPriority | 2 | scheduled less often than NormalPriority. |
QThread::NormalPriority | 3 | the default priority of the operating system. |
QThread::HighPriority | 4 | scheduled more often than NormalPriority. |
QThread::HighestPriority | 5 | scheduled more often than HighPriority. |
QThread::TimeCriticalPriority | 6 | scheduled as often as possible. |
QThread::InheritPriority | 7 | use the same priority as the creating thread. This is the default. |
此外,QThread
類還提供了 yieldCurrentThread()
靜態函數,該函數是在通知作業系統「我這個執行緒不重要,優先處理其他執行緒吧」。當然,呼叫該函數後不會立馬將 CPU 計算資源交出去,而是由作業系統決定。
QThread
類還提供了 sleep()
、msleep()
、usleep()
這三個函數,這三個函數也是在通知作業系統「在未來 time 時間內我不參與 CPU 計算」。
值得注意的是:usleep()
並 不能保證準確性 。某些OS可能將舍入時間設定為10us/15us
;在 Windows
上它將四捨五入為 1ms
的倍數。
其實上文已經演示了兩種方式:
共用記憶體
執行緒隸屬於某一個程序,與程序內的其他執行緒一起共用這片地址空間。
訊息傳遞
藉助Qt的訊號槽&事件迴圈機制。
說個題外話,在 Android
中,UI操作只能在主執行緒中進行。這種情況在Qt中其實類似,那麼當我們子執行緒需要更新UI控制元件時怎麼處理呢?很簡單傳送訊號讓主執行緒更新即可~
雖然使用多執行緒的思想是讓程式儘可能並行執行,但是總有一些時候,執行緒必須停止以等待其他執行緒。例如兩個執行緒同時寫全域性變數,由於寫入操作相對於CPU不具備原子性,結果通常具有不確定性。
QMutex
,任意時刻至多有一個執行緒可以使用該鎖,若一個執行緒嘗試獲取 mutex
,而此時 mutex
已被鎖住。則這兒執行緒將休眠直到 mutex解鎖
為止。互斥鎖經常用於共用資料。
QMutex mutex;
void thread1()
{
mutex.lock();
//dosomething()
mutex.unlock();
}
void thread2()
{
mutex.lock();
//dosomething()
mutex.unlock();
}
QReadWriteLock
,與 QMutex
類似,不過它允許多個執行緒對共用資料進行讀取。使用它替代 QMutex
可提高多執行緒程式的並行度。
QReadWriteLock lock;
void ReaderThread::run()
{
...
lock.lockForRead();
read_file();
lock.unlock();
...
}
void WriterThread::run()
{
...
lock.lockForWrite();
write_file();
lock.unlock();
...
}
QSemaphore
,QMutex
的一般化,用於保護一定數量的相同的資源。典型的是 生成者-消費者。
QSemaphore sem(5); // sem.available() == 5
sem.acquire(3); // sem.available() == 2
sem.acquire(2); // sem.available() == 0
sem.release(5); // sem.available() == 5
sem.release(5); // sem.available() == 10
sem.tryAcquire(1); // sem.available() == 9, returns true
sem.tryAcquire(250); // sem.available() == 9, returns false
QWaitCondition
,它允許一個執行緒在一些條件滿足的情況下喚醒其他執行緒。
QWaitCondition Class
中列舉一個接收按鍵並喚醒去其他執行緒進行處理的 demo:
forever {
mutex.lock();
keyPressed.wait(&mutex);
++count;
mutex.unlock();
do_something();
mutex.lock();
--count;
mutex.unlock();
}
forever {
getchar();
mutex.lock();
// Sleep until there are no busy worker threads
while (count > 0) {
mutex.unlock();
sleep(1);
mutex.lock();
}
keyPressed.wakeAll();
mutex.unlock();
}
在檢視 Qt Class
檔案時,有時候我們會看到執行緒安全和可重入的標記,什麼是執行緒安全,什麼是可重入?
執行緒安全
表示該函數可被多個執行緒呼叫,即使他們使用了共用資料,因為該共用資料的所有範例都被序列化了。
可重入
一個可重入的函數可被多個執行緒呼叫,但是隻能是使用自己資料的情況下。
如果每個執行緒使用一個類的不同範例,該類的成員函數可以被多個執行緒安全地呼叫,那麼該類被稱為可重入的;如果所有執行緒使用該類的相同範例,該類的成員函數也可以被多個執行緒安全地呼叫,那麼該類是執行緒安全的。
QObject
是可重入的。它的大多數 非GUI子類,如 QTimer
、QTcpSocket
也都是可重入的,可以在多執行緒中使用。值得注意的是,這些類被設計成在單一執行緒中進行建立和使用,在一個執行緒中建立一個物件,然後在另一個執行緒中呼叫這個物件的一個函數是無法保證一定可以工作的。需要滿足以下三個條件:
QObject
的子物件必須在建立它的父物件的執行緒中建立。這意味這不要將 QThread
物件 (this)
作為在該執行緒中建立的物件的父物件。QThread
物件以前,刪除在該執行緒中建立的所有物件。對於大部分 GUI類
,尤其是 QWidget及其子類,都是不可重入的,我們只能在主執行緒中使用。QCoreApplication::exec()
也必須在主執行緒中呼叫。
執行緒的切換是要消耗系統資源的,頻繁的切換執行緒會使效能降低。執行緒太少的話又不能完全發揮 CPU 的效能。
一般後端伺服器都會設定最大工作執行緒數,不同的架構師有著不同的經驗,有些業務設定為 CPU 邏輯核心數的4倍,有的甚至達到32倍。
在 Venkat Subramaniam
博士的 《Programming Concurrency on the JVM》 這本書中提到關於最優執行緒數的計算,即:
線 程 數 量 = 可 用 核 心 數 / ( 1 − 阻 塞 系 數 ) 執行緒數量 = 可用核心數/(1 - 阻塞係數) 線程數量=可用核心數/(1−阻塞系數)
可用核心數就是所有邏輯 CPU 的總數,這可以用 QThread::idealThreadCount()
靜態函數獲取,比如雙核四執行緒的 CPU 的返回值就是4。
但是阻塞係數比較難計算,這需要用一些效能分析工具來輔助計算。如果只是粗淺的計算下執行緒數,最簡單的辦法就是 CPU 核心數 * 2 + 2 。更為精細的找到最優執行緒數需要不斷的調整執行緒數量來觀察系統的負載情況。
Qt Creator快速入門
Qt5.9 C++開發指南
Qt使用多執行緒的一些心得——1.繼承QThread的多執行緒使用方法