Qt 多线程

本文最后更新于:2026年8月29日 晚上

Qt 多线程的开发目的是为了保证在进行耗时操作时,可以处理用户的其他输入输出。比如,如果在 UI 线程里面进行耗时操作,界面会不响应用户操作,以及提升程序性能。现在的电脑一般都是多核 CPU,多线程并行处理事务,可以大大提升程序的性能。

Qt 信号槽的一些事 | 渡世白玉 (dushibaiyu.com) QtConcurrent 线程使用说明 - 知乎 (zhihu.com) Qt 之 QtConcurrent-CSDN 博客

1 信号与槽的连接方式

五种连接方式

1
2
3
4
5
6
7
enum ConnectionType {
AutoConnection, //默认
DirectConnection, //立即调用
QueuedConnection, //放在接收者队列
BlockingQueuedConnection, //与放在接收者队列,同时阻塞发送者
UniqueConnection = 0x80 //标志位,避免重复链接
};

1.1 连接方式一:AutoConnection

一般信号槽不会写第五个参数,其实使用的是默认值,使用这个值则连接类型会在信号发送时决定。

  • 如果接收者和发送者在同一个线程,则自动使用 Qt::DirectConnection 类型。
  • 如果接收者和发送者不在一个线程,则自动使用 Qt::QueuedConnection 类型。

1.2 连接方式二:DirectConnection

函数会在信号发送的时候直接被调用,槽函数运行于信号发送者所在线程,有点类似于回调函数。效果看上去就像是直接在信号发送位置调用了槽函数。这个在多线程环境下比较危险,可能会造成崩溃。

1.3 连接方式三:QueuedConnection

槽函数在控制回到接收者所在线程的事件循环时被调用,槽函数运行于信号接收者所在线程。发送信号之后,槽函数不会立刻被调用,等到接收者的当前函数执行完毕,并进入信号接收者的事件循环后,槽函数才会被调用。多线程环境下一般用这个。

1.4 连接方式四:BlockingQueuedConnection

槽函数的调用时机与 Qt::QueuedConnection 一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完。使用该连接方式,接收者和发送者绝对不能在一个线程,否则程序会死锁!!!在多线程间需要同步的场合可能需要这个。

1.5 连接方式五:UniqueConnection

严格上来说,相比上面四种连接方式,它并不算新的连接方式,而是用于修饰上面的四种连接方式。它实现的效果就是避免重复连接,因为 Qt 的信号槽是可以同一个信号和槽函数重复多次连接。这种通常都会是只执行一次就好,那就通过这个标志位进行修饰,达到多次连接(实际上也只是连接了一次)也只调用一次槽函数的效果。用 (Qt::ConnectionType|Qt::UniqueConnection) 来修饰。

2 创建多线程的两种方式

2.1 继承 QThread

继承 QThread 是创建线程的一个普通方法,其中创建的线程只有 run() 方法在线程里的,其他类内定义的方法都在主线程内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class WorkerThread : public QThread {
Q_OBJECT
void run() override {
QString result;
/* ... 耗时或阻塞操作 ... */
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();
}
  • run 函数在新线程中执行,run 函数执行结束,线程结束。
  • WorkerThread 实例化的对象属于创建它的线程,而不是 run 函数所在线程。
  • WorkerThread 没有事件循环,除非在 run() 函数中调用 exec();
  • 队列连接到 WorkerThread 的 slot 函数,slot 函数在创建 WorkerThread 对象的线程中执行。
  • 直接调用 WorkerThread 的方法,该方法的执行线程为调用处的线程。

2.2 继承 QObject

继承 QObject 类的方法更加灵活,它通过 QObject::moveToThread() 函数,将一个 QObject 的类转移到一个线程里执行。

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
class Worker : public QObject {
Q_OBJECT

public slots:
void doWork( const QString& parameter ) { // 工作槽函数
QString result;
/* ... 耗时或阻塞操作 ... */
emit resultReady( result ); // 结果完成信号
}
signals:
void resultReady( const QString& result ); // 结果完成信号
};

class Controller : public QObject {
Q_OBJECT
QThread workerThread;

public:
Controller() {
Worker* worker = new Worker;
worker->moveToThread( &workerThread ); // 移入线程
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& ); // 信号 操作信号
};
  • 调用 moveToThread 函数的对象不能设置父对象。
  • Worker 类中的槽函数可以跟任意线程的任意信号建立连接,队列连接时,在新线程中执行。
  • 直接调用 Worker 类中的函数,在调用线程内执行。
  • 同时发送多个与 Worker 类中槽函数连接的信号,槽函数依次执行。

3 线程池 QThreadPool

线程池是一种线程使用模式,它管理着一组可重用的线程,可以处理分配过来的可并发执行的任务。线程池设有最大线程数,可以避免线程数过多导致的额外线程切换开销。线程池管理的线程具有可重用性,可以减少创建和销毁线程的次数。它的主要目的是减少程序员编写的重复代码,提高程序的效率和性能,在高并发的项目中会用到。

3.1 单线程代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <QDebug>
#include <QElapsedTimer>

int main( int argc, char* argv[] ) {
QString string;
string = "1000101010"; // 创建字符串
string = string.repeated( 10000000 ); // 重复 10000000 次
int count = 0; // 计数'1'

QElapsedTimer timer;
timer.start(); // 开始计时

for ( int i = 0, len = string.length(); i < len; ++i ) {
if ( string[ i ] == '1' ) {
count++;
}
}

qDebug() << "spend time: " << timer.elapsed() << "ms, count: " << count; // 统计耗时
}

3.2 线程池代码

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
#include <QDebug>
#include <QElapsedTimer>
#include <QMutex>
#include <QThreadPool>

class Counter : public QRunnable {
public:
Counter( int index, int len, QString* string, QMutex* mutex, int* count ) { // 初始化函数
m_index = index;
m_length = len;
m_string = string;
m_mutex = mutex;
m_count = count;
}

private:
void run() {
int count = 0;
for ( int i = m_index, end = m_index + m_length; i < end; ++i ) {
if ( ( *m_string )[ i ] == '1' ) {
count++;
}
}

/* 多线程操作共享变量需要加锁 */
m_mutex->lock();
*m_count += count;
m_mutex->unlock();
}

// 成员变量
int m_index;
int m_length;
QString* m_string;
QMutex* m_mutex;
int* m_count;
};

int main( int argc, char* argv[] ) {
QString string;
string = "1000101010";
string = string.repeated( 10000000 );
int count = 0;

QElapsedTimer timer;
timer.start(); // 开始计时

auto pool = QThreadPool::globalInstance(); // 创建全局线程池对象
pool->setMaxThreadCount( 8 ); // 设置最多 8 个线程

QMutex mutex;
/* 分割成 100 块,分别计算 */
for ( int i = 0, span = string.length() / 100; i < 100; i++ ) {
Counter* a = new Counter( i * span, span, &string, &mutex, &count ); // 每个计数类对象进行计数
a->setAutoDelete( true ); // 执行完后自动释放
pool->start( a );
}
pool->waitForDone(); // 等待线程全部完成

qDebug() << "spend time: " << timer.elapsed() << "ms, count: " << count; // 耗时统计
}
  • 默认情况下,run 函数执行完,线程对象会被线程池自动删除。可以使用 setAutoDelete 函数设置。
  • QThreadPool::start() 多次启动设置为 autoDelete 的 QRunnable 对象,可能导致崩溃。

4 并发编程 QtConcurrent

QtConcurrent 提供了高级 API,使编写多线程程序时,不需要使用诸如互斥锁、读写锁、等待条件或信号量等低级线程安全类。每调用一次 QtConcurrent::run() 函数,就新建立一个线程。

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
#include <QApplication>
#include <QDebug>
#include <QString>
#include <QThread>
#include "qtconcurrentrun.h"
class A {
public:
A() {
std::cout << "我是 A 的构造函数" << std::endl;
};

QString m_func( QString name ) {
m_name = name;
std::cout << "我是 A 的成员函数" << std::endl;
}
QString m_name;
};

int main( int argc, char** argv ) {
QApplication app( argc, argv );
QByteArray bytearray = "hello world";
A* a = new A();
QFuture< QString > fut1 = QtConcurrent::run( bytearray, &QByteArray::split, ',' ); // 1.调用 QByteArray 的常量成员函数 split(),传递常量引用,bytearray
QFuture< QByteArray > fut2 = QtConcurrent::run( a, &A::m_func, QString( "Thread 2" ) ); // 2.非常量成员函数运行在一个新的线程,传递指针

QString result1 = fut1.result();
QString result2 = fut2.result();

fut1.waitForFinished();
fut2.waitForFinished();
}
  • Concurrent run,在线程池内起一个线程来执行一个函数。

5 并行处理一批数据 map

5.1 更改原数据 QtConcurrent::map

1
2
3
4
5
6
7
8
9
10
11
12
void toUpper( QString& str ) {
str = str.toUpper();
}
QStringList strWords;
strWords << "Apple"
<< "Banana"
<< "cow"
<< "dog"
<< "Egg";
auto future = QtConcurrent::map( strWords, toUpper );
future.waitForFinished();
// strWords = {"APPLE", "BANANA", "COW", "DOG", "EGG"}

5.2 不改变原数据,返回处理结果 QtConcurrent::map

1
2
3
4
5
6
7
8
9
10
11
12
13
QString toUpper( const QString& str ) {
return str.toUpper();
}
QStringList strWords;
strWords << "Apple"
<< "Banana"
<< "cow"
<< "dog"
<< "Egg";
auto future = QtConcurrent::mapped( strWords, toUpper );
future.waitForFinished();
qDebug() << future.results();
// 输出:("APPLE", "BANANA", "COW", "DOG", "EGG")

5.3 处理后的结果还需要进行处理 QtConcurrent::mappedReduced

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
QString toUpper( const QString& str ) {
return str.toUpper();
}
void reduceFun( QList< QString >& dictionary, const QString& string ) {
dictionary.push_back( QString( "result: " ) + string );
}

QStringList strWords;
strWords << "Apple"
<< "Banana"
<< "cow"
<< "dog"
<< "Egg";
auto future = QtConcurrent::mappedReduced( strWords, toUpper, reduceFun );
future.waitForFinished();
qDebug() << future.result();
// 输出:("result: BANANA", "result: APPLE", "result: COW", "result: DOG", "result: EGG")
  • 注意,上述代码输出结果的顺序与原数据的顺序已经不一样了。mappedReduced 函数的逻辑是:启动多个线程执行 toUpper 对链表中的每个元素进行处理,处理后的结果再逐一交给 reduceFun 函数处理。reduceFun 并不会等 toUpper 的所有线程执行完毕后才开始执行,并且同一时刻,只有一个 reduceFun 线程在执行。如果需要使最后的输出结果顺序与输入相一致,就要用 mappedReduced 函数的第四个参数,赋值为 QtConcurrent::OrderedReduce,即可保证顺序一致。

6 过滤操作一批数据 filter

一般用于对一批数据的过滤操作。同样也包含 filter、filtered、filteredReduce 三种用法。与 QtConcurrent::map 类似,filter 函数会改变原始数据,filtered 函数将处理结果保存在 filtered 函数的返回值中,filteredReduce 会将过滤后的数据再调用 reduceFun 函数处理。这里就不再进行详细赘述,可以完全参考 map 函数的用法使用。