【Qt】多线程
目录
一.QThread的介绍和使用
1.1.常用API
1.2.finished() - 线程结束信号
1.3.简单示例
二.线程安全
2.1.QMutex
2.2.QMutexLocker
2.3.QReadWriteLocker、QReadLocker、QWriteLocker
2.4.条件变量
2.5.信号量
一.QThread的介绍和使用
事实上啊,Qt中的多线程和Linux的多线程是差不多的!!!
在Qt中,多线程的处理⼀般是通过QThread类来实现。
QThread代表⼀个在应⽤程序中可以独⽴控制的线程,也可以和进程中的其他线程共享数据。
QThread对象管理程序中的⼀个控制线程。
1.1.常用API
1. run() - 线程的⼊⼝函数
-
核心作用:这是线程的“主函数”。一个线程的生命就是从
run()
函数的开始到结束。 -
工作方式:
-
当你创建一个继承自
QThread
的子类时,你需要重写这个run()
方法。 -
在这个重写的
run()
函数里,你放置所有需要在新线程中执行的代码。 -
当
run()
函数执行完毕(执行到末尾或遇到 return),线程就自然结束了。
-
-
重要警告:
-
你永远不应该直接调用
run()
。启动线程应该使用start()
方法。如果你直接调用run()
,它就像一个普通的函数调用一样,会在当前线程中执行,而不会创建新的执行线程。
-
2. start() - 启动线程
-
核心作用:通知操作系统启动一个新的线程,并由操作系统调度该线程开始执行其
run()
函数。 -
工作方式:
-
调用
start()
后,线程进入“就绪”状态。操作系统会根据其优先级和系统负载,在某个时刻真正开始执行run()
函数。这有一个微小的延迟。 -
如果线程已经在运行,再次调用
start()
是无效的,它什么也不会做。
-
-
与
run()
的关系:start()是正确启动线程的唯一方式。start()
-> (操作系统调度) ->run()
。
3. currentThread() - 获取当前线程对象
-
核心作用:一个静态函数,返回一个指向管理当前执行线程的
QThread
指针。 -
工作方式:
-
它告诉你“当前这行代码是在哪个线程里运行的”。
-
这对于调试、日志记录以及在需要根据所在线程做出不同行为的函数中非常有用。
-
4. isRunning() - 检查线程状态
-
核心作用:查询线程是否正在活跃地执行。即,
start()
已被调用,且run()
函数还未返回。 -
工作方式:
-
返回
true
:线程已启动且尚未结束。 -
返回
false
:线程还未启动 (start()
未被调用),或者已经正常结束/被终止。
-
-
用途:通常用于条件判断,比如在关闭程序时,检查是否有工作线程还在运行。
5. sleep() / msleep() / usleep() - 线程休眠
-
核心作用:强制让当前线程暂停执行一段指定的时间。
-
区别:
-
sleep(long secs)
:休眠,单位是秒。 -
msleep(long msecs)
:休眠,单位是毫秒。 -
usleep(long usecs)
:休眠,单位是微秒。
-
-
重要警告:
-
这些是静态函数,它们让调用它们的当前线程休眠。
-
它们会阻塞当前线程。在 GUI 线程(主线程)中调用它们会导致界面卡死。
-
Qt 官方文档已不建议使用这些函数,因为它们不优雅且容易被误用。推荐使用
QTimer
或事件循环来实现非阻塞的延迟。
-
6. wait() - 等待线程结束
-
核心作用:阻塞调用它的线程,直到目标线程执行完毕或等待超时。
-
工作方式:
-
它像一个“汇合点”。比如,在主线程中调用
myThread->wait()
,那么主线程会停在这里,直到myThread
的run()
函数返回,主线程才会继续执行。 -
参数是超时时间(毫秒)。默认是
ULONG_MAX
,意味着无限等待。 -
如果线程成功结束,返回
true
;如果是因为超时而返回,则返回false
。
-
-
典型用途:
-
在程序退出时,确保所有工作线程都已完成它们的任务,再进行清理。
-
类似于 POSIX 的
pthread_join
,用于回收线程资源。
-
7. terminate() - 强制终止线程
-
核心作用:立即、强制地停止线程的执行。
-
工作方式:
-
它告诉操作系统立即终止目标线程,不管线程执行到哪一步。
-
这是极其危险的! 线程可能在修改数据结构、持有锁、或正在分配/释放内存时被突然终止,导致数据损坏、死锁或内存泄漏。
-
Qt 官方强烈不建议使用此函数,除非在万不得已的情况下(例如,线程陷入死循环且无法通过其他方式退出)。
-
1.2.finished() - 线程结束信号
-
核心作用:这是一个信号。当线程的
run()
函数执行完毕时,QThread 对象会自动发出这个信号。 -
工作方式:
-
利用 Qt 的信号槽机制,你可以将这个信号连接到一个槽函数上。
-
当线程结束时,这个槽函数就会被调用。
-
-
主要用途:
-
资源清理:最经典的用法是
connect(thread, &QThread::finished, thread, &QObject::deleteLater)
。这样,当线程结束后,线程对象自身会被安全地删除。 -
状态通知:通知其他部分(如主线程的GUI),工作已经完成,可以更新界面或进行下一步操作。
-
启动链式任务:一个线程结束时,触发另一个线程启动。
-
1.3.简单示例
创建线程的步骤如下:
-
自定义线程类
首先,创建一个自定义类,继承自QThread
。在该类中,必须重写父类的run()
函数。这个run()
函数即为线程处理函数,其内部代码将在新的子线程中执行,与主线程相互独立。 -
实现线程处理逻辑
在run()
函数中编写这个线程需要执行的任务的代码。 -
正确启动线程
启动线程时,不应直接调用run()
方法,而应通过线程对象调用start()
方法。start()
会内部调用run()
,并确保其运行在新建的线程中。 -
线程结束信号通知
可以在自定义线程类中定义信号(如finished
或自定义信号),并在run()
函数执行结束时发射该信号。这样,主线程可以通过连接该信号,执行相应的清理工作或状态更新。 -
安全关闭线程
线程执行完毕后,应适当释放资源并安全退出。可以使用quit()
和wait()
方法确保线程正确结束,避免资源泄漏。若需强制结束,可使用terminate()
,但建议仅在必要时使用,以确保程序稳定性。
通过以上步骤,能够有效地在 Qt 中使用多线程,提升程序的并发处理能力与用户体验。
我们创建一个项目
我们就使用多线程来实现一下定时器这个功能
创建另外一个线程,在新线程中实现计时。
我们需要创建一个类,继承自QThread
我们去看看
我们发现还是老问题啊,我们需要添加一下头文件啊
我们继承QThread的目的是为了重写run()
注意:我们不能在新线程来对界面进行任何修改!!
现在我们就回到我们的主程序
我们运行一下
……
……
也是实现了啊!!!
二.线程安全
2.1.QMutex
我们直接写个例子
我们先创建一个项目
我们需要创建一个类,继承自QThread
我们去看看
我们发现还是老问题啊,我们需要添加一下头文件啊
我们继承QThread的目的是为了重写run()
我们运行一下
很明显这个值不是1000000啊!!那么我们怎么进行处理呢?
我们就需要进行加锁啊
我们这次再运行一下
这次就没有问题了吧!!
2.2.QMutexLocker
在实际开发中,使用锁(lock)保护临界区时,涉及的代码逻辑往往较为复杂,很容易在某个条件分支或异常处理中遗漏解锁(unlock)操作,从而导致死锁等问题。
类似的问题也出现在动态内存管理上——如果在释放内存之前提前返回或抛出异常,就容易造成内存泄漏。
C++ 通过引入“智能指针”(smart pointer)来自动管理内存资源的释放,有效避免了上述问题。同样地,为了自动化地管理互斥量的加锁与解锁,C++11 引入了 std::lock_guard
类模板。它基于 RAII(Resource Acquisition Is Initialization)机制,在构造时锁定互斥量,在析构时自动解锁,从而保证即使发生异常也能安全释放锁。
其基本用法如下:
{std::lock_guard<std::mutex> guard(mutex);// 执行受保护的复杂逻辑// ...
} // 离开作用域时,guard 析构,自动调用 unlock
Qt 框架也借鉴了相同的设计思想,提供了 QMutexLocker
类,实现类似的功能:
{QMutexLocker locker(&mutex);// 受保护的代码段// ...
} // locker 析构时自动解锁
这种 RAII 风格的管理机制,极大地提升了代码的可靠性和可维护性,是现代 C++ 和 Qt 开发中推荐的做法。
这个的功能和上面那个加锁的是一模一样的。
得到的结果也是下面这个
这个不会出现忘记解锁的情况。
注意:
在编写多线程程序时,既可以使用 Qt 提供的锁机制(如 QMutex),也可以使用 C++ 标准库中的锁(如 std::mutex)。这两种锁本质上都是对操作系统原生锁功能的封装,因此在功能上具有一定的互通性。
从原理上来说,C++ 标准库中的锁是可以用于同步 Qt 线程的。这是因为 Qt 的线程底层仍然基于系统的线程实现(例如 pthreads 或 Windows Threads),而 C++ 的锁也是构建在同一底层机制之上的,因此它们能够识别并作用于同一个线程系统中的线程。
尽管技术上可行,但在实际开发中一般不建议混合使用不同来源的锁机制。主要原因包括:
-
一致性与可读性:统一使用同一套线程与锁机制(如全部使用 Qt 或全部使用 C++ 标准库)有助于保持代码风格一致,降低理解和维护成本。
-
避免不必要的依赖:若在 Qt 项目中大量使用 C++ 标准库的锁,可能会增加代码的复杂度,尤其在已经依赖 Qt 线程模块的情况下。
-
功能与集成度:Qt 的锁(如 QMutexLocker)与 Qt 框架的其他部分(如信号槽、事件循环)有更好的集成,使用 Qt 自带的锁能够更自然地与框架协作。
因此,虽然 C++ 标准库的锁能够用于 Qt 线程,但在 Qt 项目中,推荐优先使用 Qt 自身提供的线程同步工具,以保持架构上的一致性和框架优势。
2.3.QReadWriteLocker、QReadLocker、QWriteLocker
特点:
- QReadWriteLock 是读写锁类,⽤于控制读和写的并发访问。
- QReadLocker⽤于读操作上锁,允许多个线程同时读取共享资源。
- QWriteLocker ⽤于写操作上锁,只允许⼀个线程写⼊共享资源。
⽤途:在某些情况下,多个线程可以同时读取共享数据,但只有⼀个线程能够进⾏写操作。读写锁提 供了更⾼效的并发访问⽅式。
// 创建一个读写锁对象,用于管理对共享资源的并发访问
QReadWriteLock rwLock;// 在读操作中使用读锁
{// 创建QReadLocker对象,在构造时自动获取读锁// 多个线程可以同时持有读锁,实现并发读取QReadLocker locker(&rwLock);// 在作用域内读取共享资源// 此时其他线程也可以同时读取,但不能写入// ...} // QReadLocker在作用域结束时自动释放读锁(析构函数中解锁)// 在写操作中使用写锁
{// 创建QWriteLocker对象,在构造时自动获取写锁// 写锁是排他性的,同一时间只能有一个线程持有写锁QWriteLocker locker(&rwLock);// 在作用域内修改共享资源// 此时其他线程既不能读取也不能写入,保证数据一致性// ...} // QWriteLocker在作用域结束时自动释放写锁(析构函数中解锁)
关键点说明:
-
QReadWriteLock:提供读写锁机制,允许多个读者或一个写者访问共享资源
-
QReadLocker:
-
构造时自动加读锁
-
析构时自动解读锁
-
支持多个线程同时持有读锁
-
-
QWriteLocker:
-
构造时自动加写锁
-
析构时自动解写锁
-
写锁是排他性的,会阻塞所有其他读写操作
-
-
RAII模式:利用对象的生命周期自动管理锁的获取和释放,避免忘记解锁导致的死锁
2.4.条件变量
注意:我们这里的条件变量和Linux上的是一模一样的。
在多线程编程中,假设除了等待操作系统正在执⾏的线程之外,某个线程还必须等待某些条件满⾜才 能执⾏,这时就会出现问题。
这种情况下,线程会很⾃然地使⽤锁的机制来阻塞其他线程,因为这只 是线程的轮流使⽤,并且该线程等待某些特定条件,⼈们会认为需要等待条件的线程,在释放互斥锁 或读写锁之后进⼊了睡眠状态,这样其他线程就可以继续运⾏。当条件满⾜时,等待条件的线程将被 另⼀个线程唤醒。
在Qt中,专⻔提供了QWaitCondition类来解决像上述这样的问题。
特点:QWaitCondition是Qt框架提供的条件变量类,⽤于线程之间的消息通信和同步。
⽤途:在某个条件满⾜时等待或唤醒线程,⽤于线程的同步和协调。
什么是 QWaitCondition?
你可以把 QWaitCondition
想象成一个线程间的“协调员”或“信号灯”。它的核心作用是:让一个线程在某些条件不满足时主动进入等待(休眠)状态,并在另一个线程使条件满足后,被唤醒并继续执行。
它解决了什么问题?在没有 QWaitCondition
的情况下,如果线程需要等待某个条件,它可能不得不使用“忙等待”(busy-waiting),即在一个循环里不停地检查条件,这非常消耗CPU资源。QWaitCondition
提供了一种高效的方式,让线程在等待时“睡觉”,不占用CPU,直到条件满足时才被唤醒。
核心要点: QWaitCondition
必须与一个 互斥锁(QMutex 或 QReadWriteLock) 配合使用。这个锁用于保护“条件”本身(即那个共享的、线程不安全的状态变量)。
常用接口详解
让我们来逐一剖析 QWaitCondition
的几个关键成员函数。
1. bool wait(QMutex *lockedMutex, QDeadlineTimer deadline = QDeadlineTimer(QDeadlineTimer::Forever))
(这是 Qt 5.15/6 推荐的带超时参数的新接口,比老式的 wait(mutex, unsigned long time)
更现代)
-
功能:这是最核心的等待函数。调用它的线程会释放它已经锁定的
lockedMutex
,然后使当前线程进入等待(阻塞)状态。-
等待被唤醒:直到其他线程调用了
wakeOne()
或wakeAll()
来唤醒它。 -
等待超时:或者直到
deadline
指定的超时时间到达。
-
-
参数:
-
lockedMutex
:一个已经被当前线程锁定的互斥锁的指针。这是关键!在调用wait()
之前,你必须先lock()
这个锁。 -
deadline
:一个QDeadlineTimer
对象,指定等待的最晚期限。默认是Forever
,意味着无限期等待。
-
-
返回值:
-
true
:如果线程是被wakeOne()
或wakeAll()
唤醒的。 -
false
:如果是因为超时而唤醒的。
-
-
内部执行流程(非常重要!):
-
前提:当前线程已经成功锁定了
lockedMutex
。 -
调用
wait(...)
。 -
系统原子性地执行两个操作:
a. 释放lockedMutex
(让其他线程可以获取它来修改条件)。
b. 将当前线程挂起(进入睡眠)。-
“原子性”意味着这两个操作不可分割,避免了竞争条件。
-
-
当线程被唤醒(或因超时醒来)时,函数在返回前,会重新尝试获取(锁定)
lockedMutex
。所以当wait()
函数返回时,当前线程再次持有了这个锁。
-
-
典型使用模式(伪代码):
// 等待者线程 (Consumer) mutex.lock(); // 第一步:先上锁,保护下面的“条件检查” while (!conditionIsMet) { // 第二步:用循环检查条件是否满足(防止虚假唤醒)// 条件不满足,开始等待bool wokeBySignal = waitCondition.wait(&mutex, timeout);if (!wokeBySignal) {// 处理超时逻辑}// 如果被唤醒,会再次循环检查 conditionIsMet 是否真的为 true } // ... 条件满足了,做相应的工作 ... mutex.unlock(); // 第三步:工作做完后,解锁
2. void wakeOne()
-
功能:唤醒一个正在该
QWaitCondition
上等待的线程。具体唤醒哪一个是不确定的,由操作系统的调度器决定。 -
使用场景:当你知道条件满足后,只需要唤醒一个线程就足够时使用。例如,在生产者-消费者模型中,生产者只生产了一个数据项,只需要唤醒一个消费者来处理它。
-
用法:
// 唤醒者线程 (Producer) mutex.lock(); // ... 修改共享数据,使条件变为满足状态 ... conditionIsMet = true; waitCondition.wakeOne(); // 通知一个等待的线程 mutex.unlock();
注意:通常建议在持有互斥锁的情况下调用
wakeOne()
。这样可以保证,在你修改条件和发送唤醒信号之间,不会有其他线程插足,避免了某些微妙的竞争条件。
3. void wakeAll()
-
功能:唤醒所有正在该
QWaitCondition
上等待的线程。 -
使用场景:当条件满足后,所有等待的线程都有可能继续工作时使用。例如:
-
一个资源从“不可用”变为“可用”,所有等待该资源的线程都可以来竞争。
-
你希望关闭程序,需要通知所有等待的工作线程退出。
-
-
用法:与
wakeOne()
类似,只是调用的是wakeAll()
。mutex.lock(); // ... 修改共享数据 ... globalResourceAvailable = true; waitCondition.wakeAll(); // 通知所有等待的线程 mutex.unlock();
总的来说,条件变量的使用就像是下面这样子
// 创建互斥锁和条件变量
QMutex mutex;
QWaitCondition condition;// 在等待线程中
mutex.lock(); // 获取互斥锁,保护共享资源的访问// 检查条件是否满足,使用while循环防止虚假唤醒
while (!conditionFullfilled())
{// 条件不满足时等待// wait()会暂时释放mutex并让线程进入等待状态// 当被唤醒时,它会重新获取mutex然后继续执行condition.wait(&mutex);
}// 条件满足后继续执行相关操作
// ...
mutex.unlock(); // 释放互斥锁// 在改变条件的线程中
mutex.lock(); // 获取互斥锁,保护对共享条件的修改// 改变条件(通常是修改某些共享变量)
changeCondition();// 唤醒所有等待该条件的线程
// 这些线程会从condition.wait()中返回并重新检查条件
condition.wakeAll(); mutex.unlock(); // 释放互斥锁
我们这里不细讲这个条件变量,感兴趣的可以去:【Linux】多线程4——线程同步/条件变量_linux 线程同步-CSDN博客
2.5.信号量
有时在多线程编程中,需要确保多个线程可以相应的访问⼀个数量有限的相同资源。例如,运⾏程序 的设备可能是⾮常有限的内存,因此我们更希望需要⼤量内存的线程将这⼀事实考虑在内,并根据可 ⽤的内存数量进⾏相关操作,多线程编程中类似问题通常⽤信号量来处理。
信号量类似于增强的互斥 锁,不仅能完成上锁和解锁操作,⽽且可以跟踪可⽤资源的数量。
特点:QSemaphore是Qt框架提供的计数信号量类,⽤于控制同时访问共享资源的线程数量。
⽤途:限制并发线程数量,⽤于解决⼀些资源有限的问题。
QSemaphore semaphore(2); //同时允许两个线程访问共享资源//在需要访问共享资源的线程中semaphore.acquire(); //尝试获取信号量,若已满则阻塞//访问共享资源//...semaphore.release(); //释放信号量//在另⼀个线程中进⾏类似操作
我们这里不细讲这个信号量,感兴趣的可以去:【Linux】多线程6——POSIX信号量,环形队列cp问题_posixpv操作-CSDN博客