1. 程式人生 > >Qt使用多執行緒的一些心得——1.繼承QThread的多執行緒使用方法

Qt使用多執行緒的一些心得——1.繼承QThread的多執行緒使用方法

1.摘要
Qt有兩種多執行緒的方法,其中一種是繼承QThread的run函式,另外一種是把一個繼承於QObject的類轉移到一個Thread裡。
Qt4.8之前都是使用繼承QThread的run這種方法,但是Qt4.8之後,Qt官方建議使用第二種方法。兩種方法區別不大,用起來都比較方便,但繼承QObject的方法更加靈活。這裡要記錄的是如何正確的建立一個執行緒,特別是如何正確的退出一個執行緒。

本文先介紹QThread的普通用法,這個用法可能網上很多文章都介紹過,如果已經瞭解大可跳過此節,本文重點介紹執行緒退出的幾種方法,根據需求正確的建立和退出執行緒等問題。

2.Qt多執行緒方法1 繼承QThread
在使用繼承QThread的run方法之前需要了解一條規則:

QThread只有run函式是在新執行緒裡的,其他所有函式都在QThread生成的執行緒裡

QThread只有run函式是在新執行緒裡的
QThread只有run函式是在新執行緒裡的
QThread只有run函式是在新執行緒裡的

重要的事情說3遍!!!

,如果QThread是在ui所在的執行緒裡生成,那麼QThread的其他非run函式都是和ui執行緒一樣的,所以,QThread的繼承類的其他函式儘量別要有太耗時的操作,要確保所有耗時的操作都在run函式裡。
在UI執行緒下呼叫QThread的非run函式(其實也不應該直接呼叫run函式,而應該使用start函式),和執行普通函式無區別,這時,如果這個函式要對QThread的某個變數進行變更,而這個變數在run函式裡也會被用到,這時就需要注意加鎖的問題,因為可能這個變數前幾毫秒剛剛在run中呼叫,再呼叫時已經被另外的執行緒修改了。

2.1寫一個繼承於QThread的執行緒
本文的重點不是教會你繼承run寫一個多執行緒,任何有程式設計基礎的5分鐘就能學會使用QThread的方法,本文真正要講的是後面那幾節,如如何安全的退出一個執行緒,如何開啟一個臨時執行緒,執行結束馬上關閉等問題。如果你對QThread有初步瞭解,那麼可以略過這節,但你最好看看這節後面提出的幾個問題。

任何繼承於QThread的執行緒都是通過繼承QThread的run函式來實現多執行緒的,因此,必須重寫QThread的run函式,把複雜邏輯寫在QThread的run函式中。

看看一個普通繼承QThread的例子:
標頭檔案:

ifndef THREADFROMQTHREAD_H

define THREADFROMQTHREAD_H

include

class ThreadFromQThread : public QThread
{
Q_OBJECT
signals:
void message(const QString& info);
void progress(int present);
public:
ThreadFromQThread(QObject* par);
~ThreadFromQThread();
void setSomething();
void getSomething();
void setRunCount(int count);
void run();
void doSomething();
private:
int m_runCount;
};

endif // THREADFROMQTHREAD_H

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cpp檔案:

include “ThreadFromQThread.h”

include

ThreadFromQThread::ThreadFromQThread(QObject* par) : QThread(par)
,m_runCount(20)
{

}

ThreadFromQThread::~ThreadFromQThread()
{
qDebug() << “ThreadFromQThread::~ThreadFromQThread()”;
}

void ThreadFromQThread::setSomething()
{
msleep(500);
QString str = QString(“%1->%2,thread id:%3”).arg(FUNCTION).arg(FILE).arg((int)QThread::currentThreadId());
emit message(str);
}

void ThreadFromQThread::getSomething()
{
msleep(500);
emit message(QString(“%1->%2,thread id:%3”).arg(FUNCTION).arg(FILE).arg((int)QThread::currentThreadId()));
}

void ThreadFromQThread::setRunCount(int count)
{
m_runCount = count;
emit message(QString(“%1->%2,thread id:%3”).arg(FUNCTION).arg(FILE).arg((int)QThread::currentThreadId()));
}

void ThreadFromQThread::run()
{
int count = 0;
QString str = QString(“%1->%2,thread id:%3”).arg(FILE).arg(FUNCTION).arg((int)QThread::currentThreadId());
emit message(str);
while(1)
{
sleep(1);
++count;
emit progress(((float)count / m_runCount) * 100);
emit message(QString(“ThreadFromQThread::run times:%1”).arg(count));
doSomething();
if(m_runCount == count)
{
break;
}
}
}

void ThreadFromQThread::doSomething()
{
msleep(500);
emit message(QString(“%1->%2,thread id:%3”).arg(FUNCTION).arg(FILE).arg((int)QThread::currentThreadId()));
}
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
46
47
48
49
50
51
52
53
54
55
56
57
這個簡單的例子有一個Qt類常見的內容,包含了普通方法,訊號槽,和一個run函式。這裡函式setSomething();進行了500ms的延遲,getSomething同理。這是為了驗證在QThread::run()之外呼叫QThread成員函式不會執行在新執行緒裡。

上面程式碼用到了QThread::currentThreadId()這是一個靜態函式,用於返回當前執行緒控制代碼,這個值除了區分以外沒有別的用處。

為了驗證這個執行緒,編寫一個簡單的介面,這個介面主要用於驗證如下幾個問題:、
- 在UI執行緒呼叫setSomething();函式和getSomething();函式會不會卡頓?
- 在UI執行緒呼叫QThread::quit()或QThread::exit()函式會不會停止執行緒?
- 在UI執行緒呼叫QThread::terminate函式會不會停止執行緒?
- 如何正確的退出執行緒?

2.2 QThread的幾個函式quit、exit、terminate函式
為了驗證上面這些,編寫一個簡單的介面如下圖所示:

這裡寫圖片描述

include “Widget.h”

include “ui_Widget.h”

include “ThreadFromQThread.h”

include

Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
//控制元件初始化
ui->progressBar->setRange(0,100);
ui->progressBar->setValue(0);
ui->progressBar_heart->setRange(0,100);
ui->progressBar_heart->setValue(0);
//按鈕的訊號槽關聯
connect(ui->pushButton_qthread1,&QPushButton::clicked
,this,&Widget::onButtonQThreadClicked);
connect(ui->pushButton_qthread1_setSomething,&QPushButton::clicked
,this,&Widget::onButtonQthread1SetSomethingClicked);
connect(ui->pushButton_qthread1_getSomething,&QPushButton::clicked
,this,&Widget::onButtonQthread1GetSomethingClicked);
connect(ui->pushButton_qthreadQuit,&QPushButton::clicked
,this,&Widget::onButtonQthreadQuitClicked);
connect(ui->pushButton_qthreadTerminate,&QPushButton::clicked
,this,&Widget::onButtonQthreadTerminateClicked);
connect(ui->pushButton_qthreadExit,&QPushButton::clicked
,this,&Widget::onButtonQThreadExitClicked);
connect(ui->pushButton_doSomthing,&QPushButton::clicked
,this,&Widget::onButtonQThreadDoSomthingClicked);
connect(ui->pushButton_qthreadRunLocal,&QPushButton::clicked
,this,&Widget::onButtonQThreadRunLoaclClicked);
//
connect(ui->pushButton_qobjectStart,&QPushButton::clicked
,this,&Widget::onButtonObjectMove2ThreadClicked);
connect(ui->pushButton_objQuit,&QPushButton::clicked
,this,&Widget::onButtonObjectQuitClicked);
//
connect(&m_heart,&QTimer::timeout,this,&Widget::heartTimeOut);
m_heart.setInterval(100);
//全域性執行緒的建立
m_thread = new ThreadFromQThread(this);
connect(m_thread,&ThreadFromQThread::message
,this,&Widget::receiveMessage);
connect(m_thread,&ThreadFromQThread::progress
,this,&Widget::progress);
connect(m_thread,&QThread::finished
,this,&Widget::onQThreadFinished);

m_heart.start();

}

Widget::~Widget()
{
qDebug() << “start destroy widget”;
m_thread->stopImmediately();//由於此執行緒的父物件是Widget,因此退出時需要進行判斷
m_thread->wait();
delete ui;
qDebug() << “end destroy widget”;
}

void Widget::onButtonQThreadClicked()
{
ui->progressBar->setValue(0);
if(m_thread->isRunning())
{
return;
}
m_thread->start();
}

void Widget::progress(int val)
{
ui->progressBar->setValue(val);
}

void Widget::receiveMessage(const QString &str)
{
ui->textBrowser->append(str);
}

void Widget::heartTimeOut()
{
static int s_heartCount = 0;
++s_heartCount;
if(s_heartCount > 100)
{
s_heartCount = 0;
}
ui->progressBar_heart->setValue(s_heartCount);
}

void Widget::onButtonQthread1SetSomethingClicked()
{
m_thread->setSomething();
}

void Widget::onButtonQthread1GetSomethingClicked()
{
m_thread->getSomething();
}

void Widget::onButtonQthreadQuitClicked()
{
ui->textBrowser->append(“m_thread->quit() but not work”);
m_thread->quit();
}

void Widget::onButtonQthreadTerminateClicked()
{
m_thread->terminate();
}

void Widget::onButtonQThreadDoSomthingClicked()
{
m_thread->doSomething();
}

void Widget::onButtonQThreadExitClicked()
{
m_thread->exit();
}

void Widget::onQThreadFinished()
{
ui->textBrowser->append(“ThreadFromQThread finish”);
}
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
介面為上面提到的幾個問題提供了按鈕, 介面有一個心跳進度條,它是主程式的定時器控制,每100ms觸發用於證明主程式的ui執行緒沒有卡死。第二個進度條由執行緒控制。

點選”QThread run”按鈕,觸發onButtonQThreadClicked槽,子執行緒會執行,子執行緒執行起來後,會列印

../QtThreadTest/ThreadFromQThread.cpp->run,thread id:2900388672

可以確定執行緒執行的id是2900388672
子執行緒是個迴圈,每次迴圈都會有列印資訊:

ThreadFromQThread::run times:1
doSomething->../QtThreadTest/ThreadFromQThread.cpp,thread id:2900388672
ThreadFromQThread::run times:2
doSomething->../QtThreadTest/ThreadFromQThread.cpp,thread id:2900388672

doSomething是在run函式裡呼叫,其執行緒id是2900388672,可見這時doSomething函式是執行在子執行緒裡的。

這時,我在介面點選getSomething,setSomething,doSomething會列印:

getSomething->../QtThreadTest/ThreadFromQThread.cpp,thread id:3021526784
setSomething->../QtThreadTest/ThreadFromQThread.cpp,thread id:3021526784
doSomething->../QtThreadTest/ThreadFromQThread.cpp,thread id:3021526784

說明在非run函式裡呼叫QThread的成員函式,並不是線上程裡執行(3021526784是widget所線上程)

這時我點選quit,thread並沒進行任何處理,QThread在不呼叫exec()情況下是exit函式和quit函式是沒有作用的。

m_thread->quit() but not work

點選terminate按鈕,執行緒馬上終止,列印:

ThreadFromQThread finish

動態圖如下圖所示:

因此可以看出quit和exit函式都不會中途終端執行緒,要馬上終止一個執行緒可以使用terminate函式,但這個函式存在非常不安定因素,不推薦使用。那麼如何安全的終止一個執行緒呢?

2.3 正確的終止一個執行緒
最簡單的方法是新增一個bool變數,通過主執行緒修改這個bool變數來進行終止,但這樣有可能引起訪問衝突,需要加鎖
我們需要在原來的標頭檔案加上如下語句:

include

class ThreadFromQThread : public QThread
{
………..
public slots:
void stopImmediately();
private:
QMutex m_lock;
bool m_isCanRun;
………
};
1
2
3
4
5
6
7
8
9
10
11
12
run函式需要進行修改:

void ThreadFromQThread::stopImmediately()
{
QMutexLocker locker(&m_lock);
m_isCanRun = false;
}

void ThreadFromQThread::run()
{
int count = 0;
m_isCanRun = true;//標記可以執行
QString str = QString(“%1->%2,thread id:%3”).arg(FILE).arg(FUNCTION).arg((unsigned int)QThread::currentThreadId());
emit message(str);
while(1)
{
sleep(1);
++count;
emit progress(((float)count / m_runCount) * 100);
emit message(QString(“ThreadFromQThread::run times:%1”).arg(count));
doSomething();
if(m_runCount == count)
{
break;
}

    {
        QMutexLocker locker(&m_lock);
        if(!m_isCanRun)//在每次迴圈判斷是否可以執行,如果不行就退出迴圈
        {
            return;
        }
    }
}

}
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
QMutexLocker可以安全的使用QMutex,以免忘記解鎖(有點類似std::unique_ptr),這樣每次迴圈都會看看是否要馬上終止。
線上程需要馬上退出時,可以在外部呼叫stopImmediately()函式終止執行緒,之前的例子可以知道,由於在主執行緒呼叫QThread非run()函式的函式都是在主執行緒執行,因此,在主執行緒呼叫類似m_thread->stopImmediately()會幾乎馬上把執行緒的成員變數m_isCanRun設定為false(面對多執行緒問題要用面向過程的思維思考),因此在子執行緒的run函式的迴圈中遇到m_isCanRun的判斷後就會退出run函式,繼承QThread的函式在執行完run函式後就視為執行緒完成,會發射finish訊號。

2.4 如何正確啟動一個執行緒
執行緒的啟動有幾種方法,這幾種方法設計到它的父物件歸屬問題,和如何刪除他的問題。首先要搞清楚這個執行緒是否和UI的生命週期一致,直到UI結束執行緒才結束,還是這個執行緒只是臨時生成,等計算完成就銷燬。

第一種情況的執行緒在建立時會把生成執行緒的窗體作為它的父物件,這樣窗體結束時會自動析構執行緒的物件。但這時候要注意一個問題,就是窗體結束時執行緒還未結束如何處理,如果沒有處理這種問題,你會發現關閉視窗時會導致程式崩潰。往往這種執行緒是一個監控執行緒,如監控某個埠的執行緒。為了好區分,暫時叫這種叫全域性執行緒,它在UI的生命週期中都存在。

第二種情況是一種臨時執行緒,這種執行緒一般是突然要處理一個大計算,為了不讓UI假死需要觸發的執行緒,這時需要注意一個問題,就是線上程還沒計算完成,使用者突然終止或變更時如何處理,這種執行緒往往更多見且更容易出錯,如開啟一個大檔案,顯示一個大圖片,使用者可能看一個大圖片還沒等圖片處理完成又切換到下一個圖片,這時繪圖執行緒要如何處理才能順利解決?為了好區分,暫時叫這種叫區域性執行緒,它在UI的生命週期中僅僅是某時刻才會觸發,然後銷燬。

這就涉及到如何終止正在執行的執行緒這個問題!

2.4.1正確的啟動一個全域性執行緒(和UI一直存在的執行緒)
我發現大部分網上的教程都是教你建立一個全域性的執行緒,但往往這種執行緒用的不多,也比較好管理,需要注意的是程式退出時對執行緒的處理問題。
在ui的標頭檔案中宣告一個執行緒的指標

widget.h:

ThreadFromQThread* m_thread;
1
wodget.cpp:

class Widget : public QWidget
{
Q_OBJECT

public:
explicit Widget(QWidget *parent = 0);
~Widget();
private slots:
void onButtonQThreadClicked();
void onButtonQthread1SetSomethingClicked();
void onButtonQthread1GetSomethingClicked();
void onButtonQthreadQuitClicked();
void onButtonQthreadTerminateClicked();
void onButtonQThreadDoSomthingClicked();
void onQThreadFinished();
……
void progress(int val);
void receiveMessage(const QString& str);
void heartTimeOut();
private:
Ui::Widget *ui;
ThreadFromQThread* m_thread;
QTimer m_heart;
……
};
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
先看窗體生成的建構函式

Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
,m_objThread(NULL)
{

ui->setupUi(this);
//控制元件初始化
ui->progressBar->setRange(0,100);
ui->progressBar->setValue(0);
ui->progressBar_heart->setRange(0,100);
ui->progressBar_heart->setValue(0);
//按鈕的訊號槽關聯
connect(ui->pushButton_qthread1,&QPushButton::clicked
,this,&Widget::onButtonQThreadClicked);
connect(ui->pushButton_qthread1_setSomething,&QPushButton::clicked
,this,&Widget::onButtonQthread1SetSomethingClicked);
connect(ui->pushButton_qthread1_getSomething,&QPushButton::clicked
,this,&Widget::onButtonQthread1GetSomethingClicked);
connect(ui->pushButton_qthreadQuit,&QPushButton::clicked
,this,&Widget::onButtonQthreadQuitClicked);
connect(ui->pushButton_qthreadTerminate,&QPushButton::clicked
,this,&Widget::onButtonQthreadTerminateClicked);
connect(ui->pushButton_doSomthing,&QPushButton::clicked
,this,&Widget::onButtonQThreadDoSomthingClicked);
//心跳的關聯
connect(&m_heart,&QTimer::timeout,this,&Widget::heartTimeOut);
m_heart.setInterval(100);
//全域性執行緒的建立
//全域性執行緒建立時可以把窗體指標作為父物件
m_thread = new ThreadFromQThread(this);
//關聯執行緒的訊號和槽
connect(m_thread,&ThreadFromQThread::message
,this,&Widget::receiveMessage);//
connect(m_thread,&ThreadFromQThread::progress
,this,&Widget::progress);
connect(m_thread,&QThread::finished
,this,&Widget::onQThreadFinished);
//UI心跳開始
m_heart.start();
}
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
由於是全域性存在的執行緒,因此在窗體建立時就建立執行緒,可以把執行緒的父物件設定為窗體,這時需要注意,別手動delete執行緒指標。用於你的QThread是在Qt的事件迴圈裡面,手動delete會發生不可預料的意外。理論上所有QObject都不應該手動delete,如果沒有多執行緒,手動delete可能不會發生問題,但是多執行緒情況下delete非常容易出問題,那是因為有可能你要刪除的這個物件在Qt的事件迴圈裡還排隊,但你卻已經在外面刪除了它,這樣程式會發生崩潰。

如果你確實要刪除,請參閱void QObject::deleteLater () [slot]這個槽,這個槽非常有用,尤其是對區域性執行緒來說。後面會經常用到它用於安全的結束執行緒。

在需要啟動執行緒的地方呼叫start函式即可啟動執行緒。

void Widget::onButtonQThreadClicked()
{
ui->progressBar->setValue(0);
if(m_thread->isRunning())
{
return;
}
m_thread->start();
}
1
2
3
4
5
6
7
8
9
如果執行緒已經執行,你重複呼叫start其實是不會進行任何處理。

一個全域性執行緒就那麼簡單,要用的時候start一下就行。真正要注意的是如何在ui結束時把執行緒安全退出。

在widget的解構函式應該這樣寫:

Widget::~Widget()
{
qDebug() << “start destroy widget”;
m_thread->stopImmediately();
m_thread->wait();
delete ui;
qDebug() << “end destroy widget”;
}
1
2
3
4
5
6
7
8
這裡要注意的是m_thread->wait();這一句,這一句是主執行緒等待子執行緒結束才能繼續往下執行,這樣能確保過程是單一往下進行的,也就是不會說子執行緒還沒結束完,主執行緒就destrioy掉了(m_thread的父類是主執行緒視窗,主執行緒視窗如果沒等子執行緒結束就destroy的話,會順手把m_thread也delete這時就會奔潰了),因此wait的作用就是掛起,一直等到子執行緒結束。

還有一種方法是讓QThread自己刪除自己,就是在new執行緒時,不指定父物件,通過繫結void QObject::deleteLater () [slot]槽讓它自動釋放。這樣在widget析構時可以免去m_thread->wait();這句。

2.4.2 如何啟動一個區域性執行緒(用完即釋放的執行緒)
啟動一個區域性執行緒(就是執行完自動刪除的執行緒)方法和啟動全域性執行緒差不多,但要關聯多一個槽函式,就是之前提到的void QObject::deleteLater () [slot],這個槽函式是能安全釋放執行緒資源的關鍵(直接delete thread指標不安全)。

簡單的例子如下:

void Widget::onButtonQThreadRunLoaclClicked()
{
//區域性執行緒的建立的建立
ThreadFromQThread* thread = new ThreadFromQThread(NULL);//這裡父物件指定為NULL
connect(thread,&ThreadFromQThread::message
,this,&Widget::receiveMessage);
connect(thread,&ThreadFromQThread::progress
,this,&Widget::progress);
connect(thread,&QThread::finished
,this,&Widget::onQThreadFinished);
connect(thread,&QThread::finished
,thread,&QObject::deleteLater);//執行緒結束後呼叫deleteLater來銷燬分配的記憶體
thread->start();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
這個例子還是啟動之前的執行緒,但不同的是:
- new ThreadFromQThread(NULL);並沒有給他指定父物件
- connect(thread,&QThread::finished ,thread,&QObject::deleteLater);執行緒結束後呼叫deleteLater來銷燬分配的記憶體。
再執行緒執行完成,發射finished訊號後會呼叫deleteLater函式,在確認訊息迴圈中沒有這個執行緒的物件後會銷燬。

但是要注意避免重複點按鈕重複呼叫執行緒的情況,對於一些需求,執行緒開啟後再點選按鈕不會再重新生成執行緒,一直等到當前執行緒執行完才能再次點選按鈕,這種情況很好處理,加個標記就可以實現,也一般比較少用。

另外更多見的需求是,再次點選按鈕,需要終結上次未執行完的執行緒,重新執行一個新執行緒。這種情況非常多見,例如一個普通的圖片瀏覽器,都會有下一張圖和上一張圖這種按鈕,瀏覽器載入圖片一般都線上程裡執行(否則點選超大圖片時圖片瀏覽器會類似卡死的狀態),使用者點選下一張圖片時需要終止正在載入的當前圖片,載入下一張圖片。你不能要求客戶要當前圖片載入完才能載入下一張圖片,這就幾乎淪為單執行緒了。這時候,就需要終止當前執行緒,開闢新執行緒載入下一個圖片。

這時,上面的函式將會是大概這個樣子的

UI的標頭檔案需要一個成員變數記錄正在執行的執行緒

private slots:
void onLocalThreadDestroy(QObject* obj);
private:
QThread* m_currentRunLoaclThread;
1
2
3
4
執行生成臨時執行緒的函式將變為

void Widget::onButtonQThreadRunLoaclClicked()
{
//區域性執行緒的建立的建立
if(m_currentRunLoaclThread)
{
m_currentRunLoaclThread->stopImmediately();
}
ThreadFromQThread* thread = new ThreadFromQThread(NULL);
connect(thread,&ThreadFromQThread::message
,this,&Widget::receiveMessage);
connect(thread,&ThreadFromQThread::progress
,this,&Widget::progress);
connect(thread,&QThread::finished
,this,&Widget::onQThreadFinished);
connect(thread,&QThread::finished
,thread,&QObject::deleteLater);//執行緒結束後呼叫deleteLater來銷燬分配的記憶體
connect(thread,&QObject::destroyed,this,&Widget::onLocalThreadDestroy);
thread->start();
m_currentRunLoaclThread = thread;
}

void Widget::onLocalThreadDestroy(QObject *obj)
{
if(qobject_cast