Qt之多线程和并发_P1

多线程和并发技术是一项非常重要的功能,既可以防止耗时操作导致程序或界面出现假死现象,又可以最大化利用硬件资源,提高程序的运行效率。Qt提供了强大而灵活的多线程和并发编程支持,使得开发者可以高效地利用现代多核处理器资源。本文将详细介绍Qt多线程与并发技术,涵盖核心概念、常用类、最佳实践以及典型使用场景。我将分3三部分介绍:第一部分介绍多线程的创建和使用,线程安全和线程池,第二部分介绍线程同步,第三部分介绍多线程并发和异步。这篇文章先介绍第一部分。

文章目录

Qt线程及其使用QThread类QThread的执行逻辑QThread的退出Qt线程的使用子类化QThreadmoveToThread

线程优先级线程安全与可重入性线程安全线程可重入性两者之间的关系线程使用注意事项
线程池

Qt线程及其使用

QThread类


QThread
类继承自
QObject
,是Qt多线程编程中的核心类,它提供了一种平台无关的方式来创建和管理操作系统级线程。

QThread的执行逻辑


QThread
从调用
start()
开始执行,
start()
调用后,内部会调用线程入口函数即
QThread::run()
函数执行线程。
QThread::run()
函数是一个虚函数,默认仅实现了
exec
函数,你也可以重新实现
run()
函数,实现更高级的线程管理,
run()
函数返回后线程也就结束了。
线程开始执行时,
QThread
对象会在调用
run()
函数之前发射
started
信号,线程执行完(
run()函数返回
)后会发射
finished
信号,可以将
finished
信号与
QObject::deleteLater()
槽绑定销毁对象释放内存。

connect(thread, &QThread::finished, thread, &QThread::deleteLater);

阻塞线程可以调用
bool QThread::wait()
,当前线程会阻塞直到满足下述任一条件:

与此
QThread
对象关联的线程已完成执行(即从
run()
函数返回)。如果线程已完成,此函数返回
true
;如果线程尚未启动,也会返回
true
。到达指定的截止时间(
deadline
)此函数返回
false


QThread
对象本身属于创建它的线程(通常是主线程),而不是它所代表的新线程,只有
QThread::run
函数的执行在新线程中。

QThread的退出

可以调用
exit

quit
函数退出线程事件循环。
quit
等价于
exit(0)

exit
函数会返回到调用者,停止的只是事件处理过程,在此之后,该线程将不会再启动任何事件循环,直到再次调用
QThread::exec()
,也就是说
exit()
只对运行了事件循环(
exec()
)的线程有效!如果线程中没有运行
exec()
,则
exit()
完全无效!

run()
函数执行一半或线程还没有执行完调用
exit()
也是无法退出事件循环结束线程的,如果要想结束线程,可以调用
terminate()
函数暴力结束,此函数非常危险,不建议使用,因此这里不作过多介绍。


void MyThread::run()
{
	while(true)
	{
		qDebug() << "my thread is running!";
		msleep(100);
	}
}

//main.cpp
auto thread = new MyThread;
thread->start();
thread->exit(); //这里调用exit(),线程是不会停止运行的

所以只要没重写 run(),线程就运行了事件循环,exit() 有效。下例如果
dowork
运行完后线程没有调用
quit()

exit()
,线程是不会结束的,也不会释放
finished()
信号:


class Worker : public QObject {
	Q_OBJECT

public:
	void doWork() {
		for (int i = 0; i <= 100; i += 10) {
			qDebug() << "Worker thread:" << QThread::currentThreadId() << " progress:" << i;
			QThread::sleep(1);
		}
		emit workFinished();
	}

Q_SIGNALS:
	void workFinished();
};

//main.cpp
auto moveThread = new QThread;
auto worker = new Worker;
QObject::connect(moveThread, &QThread::started, worker, &Worker::doWork);
//QObject::connect(worker, &Worker::workFinished, moveThread, &QThread::quit);如果不调用quit()函数,线程不会结束,也不会释放finished()信号
QObject::connect(moveThread, &QThread::finished, moveThread, [] {
	qDebug() << "Worker thread finished";
	});
worker->moveToThread(moveThread);
moveThread->start();

Qt线程的使用


QThread
线程的使用有2种方式,一种是子类化
QThread
,调用
start()
函数启动线程,另一种方式是
moveToThread
将函数移动到线程中执行。

子类化QThread

即重写
run()
函数,在
run()
函数中实现需要完成的工作。如果在子类
QThread
中定义相关槽函数,这些函数不会由对象自身事件循环所执行,而是由创建子对象所在的线程(通常是主线程)来执行。


class MyThread : public QThread
{
	Q_OBJECT

public:
	void printMessage();

protected:
	void run() override;

Q_SIGNALS:
	void sendValue(int);
};

void MyThread::printMessage()
{
	qDebug() << "thread id:" << QThread::currentThreadId();
}

void MyThread::run()
{
	printMessage(); //run函数调用,printMessage()在子线程中执行
	int i = 0;
	while (++i != 100)
	{
		emit sendValue(i);
		msleep(100);
	}
}

//main.cpp
auto thread = new MyThread;
QObject::connect(thread, &QThread::finished, thread, &QObject::deleteLater);//线程结束后会释放finished的信号,利用这个信号释放内存
thread->printMessage();  //printMessage()在当前线程(主线程)而不是子线程中执行
thread->start();

注意子类化
QThread

run()
方法默认没有事件循环的,
QTimer
依赖事件循环才能工作,因此不要在
run()
中使用定时器,如下例,定时器永远不会有结果,
timeout
信号不会发射:


void run() override
{
	qDebug() << "WorkerThread started in thread:" << QThread::currentThreadId();

	// 创建一个 QTimer
	QTimer timer;
	connect(&timer, &QTimer::timeout, []() {
		qDebug() << "Timer timeout!";  //这行永远不会打印!
		});
	timer.start(500); 

	// 注意:这里没有调用 exec()!,run() 函数执行完就结束了
	qDebug() << "run() finished, thread exiting...";

	//exec(); //如果需要让定时器执行,`要手动调用`exec()`.
}

如果需要让上述定时器执行,
run()
需要手动执行
exec()
.

还需要注意的是,不要在
run()
函数中使用类的成员变量,这属于跨线程操作,非常不安全。
因此,Qt并不推荐子类化QThread方式创建线程,这种方式耦合度比较高,线程的控制和业务逻辑混在一起,难以复用,而且只要
run()
函数是在子线程中执行,一不小心就会出现跨线程操作,不安全。

moveToThread

即把需要做的工作全部封装在一个
QObject
类中,将每个任务定义为一个槽函数,再建立触发这些槽的信号,把信号和槽连接起来,然后这个类调用
moveToThread(QThread*)
函数,将任务移动到
QThread
线程中,最后调用
QThread::start
函数处理事件循环,这个对象的所有信号和槽函数都会在新的线程中执行。如果传递给
moveToThread(nullptr)
的参数为
nullptr
,所有的事件都不会执行。


class Worker : public QObject
{
	Q_OBJECT

public:
	void dowork1()
	{
		int i = 0;
		while (i++ < 10)
		{
			qDebug() << "Worker::dowork1 thread id:" << QThread::currentThreadId();
			QThread::sleep(1);
		}
	}

	void dowork2()
	{
		while (true)
		{
			qDebug() << "Worker::dowork2 thread id:" << QThread::currentThreadId();
			QThread::sleep(1);
		}
	}
};

//main.cpp
auto thread = new QThread;
auto worker = new Worker; //worker不能有父对象,否则moveToThread返回失败,dowork1或dowork2会在主线程中执行,与我们的预期相背
worker->moveToThread(thread);
QObject::connect(thread, &QThread::started, worker, &Worker::dowork1); //dowork1在新的线程中执行
QObject::connect(thread, &QThread::started, worker, &Worker::dowork2); //dowork2在dowork1结束后才执行,与dowork1执行的线程相同
thread->start();

在Qt中使用
moveToThread()
是实现多线程编程的推荐方式。使用
moveToThread()
需要注意以下几点:

对象不能有父对象


// ❌ 错误:带父对象的对象不能移动
QWidget *widget = new QWidget(parent);
worker->moveToThread(thread); // 无效!不会在新的线程中执行,Qt会输出警告

// ✅ 正确:无父对象
Worker *worker = new Worker; // parent = nullptr
worker->moveToThread(thread);

必须在对象当前亲和线程中调用
一个对象只能属于一个线程。重复调用
moveToThread()
是允许的,但需确保当前线程是其亲和线程也即对象尚未被其他线程拥有,必须在对象当前所属的线程中调用
moveToThread()
,你只能从当前线程把对象“推送”到另一个线程,不能从其他线程“拉取”对象到当前线程,唯一例外的是没有线程亲和性的对象可以被“拉取”到当前线程。


class Controller : public QObject {
	Q_OBJECT
public:
	Worker* worker;

	void setWorker(Worker* w) { worker = w; }

public slots:
	void pullWorkerHere() {
		//危险!此函数在子线程中执行
		worker->moveToThread(QThread::currentThread()); // 试图“拉取”
	}
};

Worker* worker = new Worker(); // 亲和性 = 主线程
Controller* ctrl = new Controller;
ctrl->setWorker(worker);

QThread thread;
thread.start();
ctrl->moveToThread(&thread);

// 触发 pull 操作
QMetaObject::invokeMethod(ctrl, "pullWorkerHere"); // 在 thread 中执行

上述示例中,worker所属的线程和worker调用
moveToThread
所属的线程不是同一个线程,这样做是很危险的,可能会引发崩溃或未定义行为,Qt会给出警告:


QObject::moveToThread: Current thread (0xf51017f708) is not the object's thread (0x210897df6d0).
Cannot move to target thread (0xf51017f708)

定时器会被重置
调用
moveToThread()
时,对象的所有活跃定时器会先停止,再在新线程以相同间隔重启。如果频繁在线程间移动对象,定时器事件可能被无限期推迟。因此尽量避免移动带有活跃定时器的对象,如需定时功能,考虑在目标线程中重新创建定时器。

需要注意资源的释放
使用
worker->moveToThread(thread)
,worker对象已经在子线程中,thread子线程结束结束后,子线程无法进入事件循环,也就无法触发worker的析构函数,因此在释放worker指针时需要注意在子线程
quit()
执行前释放内存空间。如下示例:


class Worker : public QObject
{
	Q_OBJECT

public:
	~Worker() {
		qDebug() << "Worker destroyed in thread:" << QThread::currentThreadId();
	}

Q_SIGNALS:
	void valueChanged(int value);
	void workFinished();

public:
	void doWork() {
		qDebug() << "Worker::doWork() running in thread:" << QThread::currentThreadId();

		// 模拟耗时操作
		for (int i = 0; i < 5; ++i) {
			qDebug() << "Working..." << i;
			Q_EMIT valueChanged(i);
			QThread::sleep(1);
		}

		Q_EMIT workFinished();
	}
};


class Controller : public QObject {
	Q_OBJECT

public:
	Controller()
	{
		_thread = new QThread(this);
		_worker = new Worker;

		connect(_worker, &Worker::valueChanged, this, &Controller::onValueChanged);

		//注意这里信号和槽的连接是从子线程到主线程的
		connect(_worker, &Worker::workFinished, this, [this] {
			qDebug() << "work finished, about to delete _worker";
			//_worker->deleteLater();//如果在这里释放_worker,其析构函数是会执行的
			_thread->quit();
			});

		connect(_thread, &QThread::started, _worker, &Worker::doWork);

		connect(_thread, &QThread::finished, this, [this] {
			qDebug() << "thread finished";
			_worker->deleteLater(); //这里子线程已经退出事件循环了,删除_worker对象也不会释放内存,_worker的析构函数不会执行
			_thread->deleteLater();
			});

		_worker->moveToThread(_thread);
		_thread->start();
	}

public:
	void onValueChanged(int value)
	{
		qDebug() << "current value=" << value << ",thread id:" << QThread::currentThreadId();
	}

private:
	Worker* _worker;
	QThread* _thread;
};

从上述打印的结果看,虽然在_thread释放的
finished
信号对应的槽函数中执行了
_worker->deleteLater()
,但是_worker的析构函数没有执行,其原因是_worker所在的子线程已经结束了,不会触发其析构函数。如果将
_worker->deleteLater()
放在
Worker::workFinished
对应的槽函数中,_worker的析构函数是会执行的,内存可以释放,但是这样做有个不好的地方是,信号和槽是从子线程到主线程,主线程调用了子线程的析构,又执行了子线程的结束,如果线程意外退出,_worker的析构函数没有成功进入,导致内存泄漏。最好的方法是,信号槽通过子线程绑定子线程的方式进行析构,这样能确保析构进入事件队列:


connect(_worker, &Worker::workFinished, _worker, &Worker::deleteLater);
connect(_worker, &Worker::workFinished, _thread, &QThread::quit);
connect(_thread, &QThread::finished, this, &QThread::deleteLater);

信号和槽机制
所有信号槽跨线程自动使用
QueuedConnection
,完全透明且安全。

线程优先级


QThread
提供了设置线程优先级的接口:
setPriority(QThread::Priority )
,它可以让系统如何为线程分配CPU时间,虽然最终调度由操作系统决定,但合理设置优先级可优化程序响应性或后台任务效率。

QThread::Priority枚举值 描述
QThread::IdlePriority 最低优先级,当没有其他线程运行时才分配
QThread::LowestPriority 较低优先级,比QThread::LowPriority稍低
QThread::LowPriority 低优先级
QThread::NormalPriority 默认优先级
QThread::HighPriority 高优先级
QThread::HighestPriority 较高优先级
QThread::TimeCriticalPriority 尽可能多的分配
QThread::InheritPriority 默认值,继承创建线程的优先级

优先级高的线程会比优先级低的线程更早获得CPU时间。设置方法一般是在线程启动前设置线程优先级。


QThread *thread = new QThread;
thread->setPriority(QThread::HighPriority); // 设置优先级
worker->moveToThread(thread);
thread->start(); // 启动后优先级生效

设置线程优先级需要注意几点:

不要滥用高优先级
高优先级(如
QThread::TimeCriticalPriority
)可能会抢占UI或其他线程的执行时间导致系统卡顿。一般是需要快速响应外部事件时才需要设置高优先级,对于不需要及时响应的后台任务(如日志记录,数据备份等)可以设置低优先级。

优先级并不能保证执行顺序
高优先级线程仍需使用mutex、QWaitCondition 等机制保护共享数据,高优先级不能保证执行顺序,优先级只是调度权重,不保证执行顺序。

避免频繁改变线程优先级
这会增加系统调度CPU的复杂性。错误的优先级设置可能导致系统不稳定或资源饥饿。

线程安全与可重入性

线程安全

所谓线程安全是指,多个线程同时调用某个函数也不会导致数据损坏、崩溃或未定义行为,即使是多个线程并发执行结果也与单线程执行的结果相同。线程安全通常是通过内部加锁实现的。


 class Counter
 {
 public:
     Counter() { n = 0; }

     void increment() { ++n; }
     void decrement() { --n; }
     int value() const { return n; }

 private:
     int n;
 };

这个类不是线程安全的,因为如果有多个线程同时尝试修改数据成员
n
,结果将是未定义的。这是因为
++

--
操作符并不总是原子操作。事实上,它们通常会被编译成三条机器指令:

将变量的值加载到寄存器中;对寄存器中的值进行加1或减1操作;将寄存器中的新值写回主内存。

如果线程A和线程B同时加载了该变量的旧值,各自在寄存器中执行加1操作,然后再将结果写回内存,那么它们的操作会相互覆盖,最终导致变量只被增加了一次!如果要想是线程安全的,可以作如下修改:


 class Counter
 {
 public:
     Counter() { n = 0; }

     void increment() { QMutexLocker locker(&mutex); ++n; }
     void decrement() { QMutexLocker locker(&mutex); --n; }
     int value() const { QMutexLocker locker(&mutex); return n; }

 private:
     mutable QMutex mutex;
     int n;
 };

线程可重入性

线程可重入性是指当一个线程正在执行该函数时,如果因某种原因(如信号、中断、递归)再次进入该函数(可能在同一线程或其他线程),不会出错。进一步地,如果一个类的成员函数在多个线程中被安全调用时,只要每个线程使用该类的不同实例,那么这个类就被称为可重入的。

下例纯函数是可重入的:


int add(int a, int b) {
    return a + b; // 没有全局状态,完全可重入
}

反例:使用静态局部变量(不可重入!)


int add(int a, int b) {
	static int sum = 0;
	sum = a+b;
	return a+b;
}

两者之间的关系

线程安全的函数总是可重入的,但可重入的函数并不总是线程安全的。也就是说可重入是一种线程安全的形式,你可以在不同线程中安全地使用各自的对象实例,但它并不能保证单个实例的多线程安全,不要让多个线程直接操作同一个对象实例,除非你自己加锁,如上述
Counter
类用
QMutex
加锁。

C++类通常是可重入的,仅仅因为它们只访问自身的成员数据。只要没有其他线程在同一时刻对同一个类实例调用成员函数,任何线程都可以安全地对该实例调用成员函数。

许多Qt类(如QObject)是可重入的,但并未被设计为线程安全的,因为若要实现线程安全,就需要反复加锁和解锁 QMutex,从而带来额外的性能开销。例如,QString 是可重入的,但不是线程安全的。你可以安全地在多个线程中同时访问不同的 QString 实例,但不能在多个线程中同时安全地访问同一个 QString 实例(除非你自己使用 QMutex 对访问进行保护)。


QObject::connect()
本身是线程安全的。在
QThread::run()
的实现中安全地发射信号(emit signals)是允许的,因为信号的发射本身是线程安全的。需要注意的是,当发送者和接收者位于不同线程时,如果接收者所在线程正在运行事件循环,则使用直接连接(Direct Connection)是不安全的.

注意:多线程领域的术语尚未完全标准化。POSIX在其C API中对“可重入”(reentrant)和“线程安全”(thread-safe)的定义与 Qt 略有不同。在将Qt与其他面向对象的C++类库结合使用时,请务必确认各方对这些术语的理解是一致的。

线程使用注意事项

在一个线程中创建对象并在另一个线程中调用其函数不保证可行,因此
QObject
的子对象必须始终在父对象创建的线程中创建。

事件驱动的对象只能在单个线程中使用。这尤其适用于定时器机制和网络模块。例如,你不能在一个并非该对象所属线程的线程中启动定时器或连接套接字。如下例,你不能在主线程中创建
QTcpSocket
对象,又在
run()
函数子线程中操作socket,Qt也会给出警告:

QSocketNotifier: Socket notifiers cannot be enabled or disabled from another thread


class MyThread : public QThread
{
	Q_OBJECT

public:
	MyThread(QObject* parent = nullptr)
		: QThread(parent)
	{
		socket = new QTcpSocket();
		socket->connectToHost("127.0.0.1", 8080);
	}
protected:
	void run() override
	{
		while (true)
		{
			socket->waitForReadyRead(1000);
			auto bytes = socket->readAll();
			qDebug() << "Read data in thread:" << QThread::currentThreadId() << ", data size:" << bytes.size();
		}
	}

private:
	QTcpSocket* socket;
};

Qt GUI类(如QWidget及其子类)是不可重入的,只能在主线程中使用,如在子线程中
new QLabel("hello")
是不允许的,
QCoreApplication::exec()
也必须在主线程中调用。

QPixmap
也是不可重入的,它与底层图像系统绑定,因此不要在非主线程中创建或操作,与之对应的
QImage
则是可重入的。

某些Qt类和函数是线程安全的,主要包括与线程相关的类(例如
QMutex
,
QMutexLocker
,
QReadWriteLock
,
QWaitCondition
等)以及一些基础函数(例如
QCoreApplication::postEvent()
,
qDebug()
)。

所有线程都支持事件过滤器(event filters),但有一个限制:监控对象(monitoring object)必须与被监控对象(monitored object)位于同一线程中。类似地,QCoreApplication::sendEvent()(与
postEvent()
不同,
postEvent()
可以跨线程投递)只能用于向调用该函数的线程中所存在的对象分发事件。

线程池

频繁创建和销毁线程会带来很大的性能开销,在需要处理大量并发任务的场景下先得尤为突出,为解决这一问题,线程池应运而生,线程池通过创建一定数量的线程放在池中,通过复用线程避免频繁创建线程带来的性能开销,避免资源耗尽。

Qt提供了
QThreadPool
以及
QRunnable
类管理线程池和线程池中的任务。
QThreadPool
用来管理一组可重用的工作线程:

自动管理线程的生命周期,默认自动删除
QRunnable
对象, 可通过调用
QRunnable::setAutoDelete()
来修改自动删除标志。可以设置线程的过期超时时间,默认为30s,可以通过
setExpiryTimeout()
进行更改,设置负数表示禁用线程过期机制;可以通过
setMaxThreadCount()
设置线程池允许使用的最大线程数,默认为CPU的核心数,可以通过函数
activeThreadCount()
返回当前正在执行任务的线程数量。支持全局线程池,每一个Qt应用程序都有一个全局线程池对象,可以通过
QThreadPool::globalInstance()
获取。

QThreadPool函数 功能说明
globalInstance() 获取全局线程池实例,每个Qt应用程序有且只有一个
start(QRunnable *runnable, int priority = 0) 提交任务到线程池,可以设置任务的优先级
startOnReservedThread(QRunnable *runnable) 释放先前通过reserveThread()预留的线程,并使用该线程来执行runnable,要求线程池中必须有至少一个“已预留但未使用”的线程槽位,如果没有预留线程,调用此函数会导致未定义行为,程序可能会崩溃
waitForDone 阻塞等待所有任务完成,可以设置超时时间
setMaxThreadCount(int maxThreadCount) 设置最大线程数,默认CPU的核心数
reserveThread() 临时增加一个可运行线程的名额,相当于临时将最大线程数+1,确保紧急任务不排队
releaseThread() 释放一个之前预留的线程槽位。必须与
reserveThread()
成对调用,否则线程池的内部状态会被破坏,导致内部计数器下溢或逻辑错乱,
activeThreadCount()
可能会返回负数
activeThreadCount() 返回当前活动线程数,包括预留线程,因此可能比maxThreadCount()返回的值还大


QRunnable
是一个接口,用于需要执行的任务或代码片段,具体行为由重写
run()
函数实现。可通过
setAutoDelete(bool)
控制任务对象在执行完毕后是否自动删除,默认自动删除,当自动删除启用时,对同一个
QRunnable
多次调用
QThreadPool::start()
会导致竞态条件(race condition),因此不建议这样做。


QRunnable
是一个抽象基类,不是继承自
QObject
,因此不能使用信号和槽。如果需要跨线程通知其他线程或对象,可以用
QMetaObject::invokeMethod


class Task : public QRunnable
{
public:
	Task(MyObject* object) :
		_object(object) {
		qDebug() << "Task thread:" << QThread::currentThreadId(); //在创建它的线程中执行(主线程)
	}

protected:
	void run() override
	{
		qDebug() << "Hello world from thread:" << QThread::currentThreadId();//在子线程中执行
		QMetaObject::invokeMethod(_object, &MyObject::onDone); //onDone函数在创建_object的线程中执行(主线程)
		QThread::sleep(1);
	}

private:
	MyObject* _object;
};
© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
妞妞哟哟哟的头像 - 鹿快
评论 抢沙发

请登录后发表评论

    暂无评论内容