📑 目录
- [1. Qt 多线程](#1. Qt 多线程)
- [1.1 Qt 多线程概述](#1.1 Qt 多线程概述)
- [1.2 QThread 常用 API](#1.2 QThread 常用 API)
- [1.3 使用线程](#1.3 使用线程)
- [1.4 线程同步](#1.4 线程同步)
- [1.4.1 QMutex 互斥锁](#1.4.1 QMutex 互斥锁)
- [1.4.2 QMutexLocker 自动锁](#1.4.2 QMutexLocker 自动锁)
- [1.4.3 QSemaphore 信号量](#1.4.3 QSemaphore 信号量)
- [1.4.4 QWaitCondition 条件变量](#1.4.4 QWaitCondition 条件变量)
- [1.5 线程间通信](#1.5 线程间通信)
- [1.6 线程池 QThreadPool](#1.6 线程池 QThreadPool)
- [1.7 多线程注意事项](#1.7 多线程注意事项)
- [1.8 总结](#1.8 总结)
- [1.9 经典面试题](#1.9 经典面试题)
1. Qt 多线程
1.1 Qt 多线程概述
在 Qt 中,多线程的处理一般是通过 QThread 类来实现。
QThread 代表一个在应用程序中可以独立控制的线程,也可以和进程中的其他线程共享数据。
QThread 对象管理程序中的一个控制线程。
💡 核心理解 :可以把
QThread想象成一个"独立的小工人",它能在后台默默干活,不阻塞主界面的操作。比如你在界面上点击按钮后,程序需要处理大量数据,如果直接在主线程中处理,界面就会卡住(无响应),而使用多线程就能让界面保持流畅。
1.2 QThread 常用 API
| API | 说明 |
|---|---|
run() |
线程的入口函数。 |
start() |
通过调用 run() 开始执行线程。操作系统将根据优先级参数调度线程。如果线程已经在运行,这个函数什么也不做。 |
currentThread() |
返回一个指向管理当前执行线程的 QThread 的指针。 |
isRunning() |
如果线程正在运行则返回 true;否则返回 false。 |
sleep() / msleep() / usleep() |
使线程休眠,单位为秒 / 毫秒 / 微秒。 |
wait() |
阻塞线程,直到满足以下任何一个条件: • 与此 QThread 对象关联的线程已经完成执行(即当它从 run() 返回时)。如果线程已经完成,这个函数将返回 true。如果线程尚未启动,它也返回 true。 • 已经过了几毫秒。如果时间是 ULONG_MAX(默认值),那么等待永远不会超时(线程必须从 run() 返回)。如果等待超时,此函数将返回 false。 这提供了与 POSIX pthread_join() 函数类似的功能。 |
terminate() |
终止线程的执行。线程可以立即终止,也可以不立即终止,这取决于操作系统的调度策略。在 terminate() 之后使用 QThread::wait() 来确保。 |
finished() |
当线程结束时会发出该信号,可以通过该信号来实现线程的清理工作。 |
💡 面试小贴士 :
run()是线程的入口,但启动线程不能直接调用run(),必须通过start()来启动,start()内部会创建新的线程并调用run()。### 1.3 使用线程程
创建线程的步骤:
- 自定义一个类,继承于
QThread,并且只有一个线程处理函数(和主线程不是同一个线程),这个线程处理函数主要就是重写父类中的run()函数。 - 线程处理函数里面写入需要执行的复杂数据处理。
- 启动线程不能直接调用
run()函数,需要使用对象来调用start()函数实现线程启动。 - 线程处理函数执行结束后可以定义一个信号来告诉主线程。
- 最后关闭线程。
示例:
1、首先新建 Qt 项目,设计 UI 界面如下:

2、新建一个类,继承于 QThread 类。

程序如下:
cpp
/******************************** timethread.h ********************************/
#ifndef TIMETHREAD_H
#define TIMETHREAD_H
#include <QThread> //添加头文件
class TimeThread : public QThread
{
Q_OBJECT
public:
TimeThread();
void run(); //线程任务函数
signals:
void sendTime(QString Time); //声明信号函数
};
#endif // TIMETHREAD_H
cpp
/******************************** widget.h ********************************/
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <timethread.h> //添加头文件
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_btn_clicked();
void showTime(QString Time);
private:
Ui::Widget *ui;
TimeThread t; //定义线程对象
};
#endif // WIDGET_H
cpp
/******************************** timethread.cpp ********************************/
#include "timethread.h"
#include <QTime>
#include <QDebug>
TimeThread::TimeThread()
{
}
void TimeThread::run()
{
while(1)
{
QString time = QTime::currentTime().toString("hh:mm:ss");
qDebug() << time;
emit sendTime(time); //发送信号
sleep(1);
}
}
cpp
/******************************** widget.cpp ********************************/
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
connect(&t,&TimeThread::sendTime,this,&Widget::showTime);
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_btn_clicked()
{
// 启动线程
t.start();
}
void Widget::showTime(QString Time)
{
// 显示时间到 Label 上
ui->label->setText(Time);
}
实现效果如下:

💡 笔记总结:
- 线程处理函数
run()中是一个死循环,不断获取当前时间并通过信号sendTime发送给主线程。- 主线程通过
connect将信号与槽showTime关联,从而更新界面。- 启动线程使用
start(),而不是直接调用run()。- 线程与主线程通过信号槽进行通信,这是 Qt 多线程的核心模式
💡 说明:1、线程函数内部不允许操作
UI图形界⾯,⼀般⽤数据处理;2、
connect()函数第五个参数表⽰的为连接的⽅式,且只有在多线程的时候才意义。
信号槽连接类型
connect() 函数的第五个参数用于指定连接类型,它决定了信号与槽函数的执行方式。以下是 Qt 中常见的连接类型:
| 连接类型 | 说明 | 适用场景 | 注意事项 |
|---|---|---|---|
Qt::AutoConnection |
根据信号和槽函数所在的线程自动选择连接类型。如果信号和槽函数在同一线程中,则使用 Qt::DirectConnection;如果它们位于不同的线程中,则使用 Qt::QueuedConnection。 |
默认连接类型,适用于大多数场景。 | 无需手动指定,由 Qt 自动判断。 |
Qt::DirectConnection |
当信号发出时,槽函数会立即在同一线程中执行。 | 信号和槽函数在同一线程中的情况,可以实现直接的函数调用。 | 需要注意线程安全性。 |
Qt::QueuedConnection |
当信号发出时,槽函数会被插入到接收对象所属线程的事件队列中,等待下一次事件循环时执行。 | 信号和槽函数在不同线程中的情况,可以确保线程安全。 | 槽函数不会立即执行,存在一定的延迟。 |
Qt::BlockingQueuedConnection |
与 Qt::QueuedConnection 类似,但发送信号的线程会被阻塞,直到槽函数执行完毕。 |
需要等待槽函数执行完毕再继续的场景。 | 需要注意可能引起线程死锁的风险。 |
Qt::UniqueConnection |
这是一个标志,可以使用位或(` | `)与上述任何一种连接类型组合使用。 | 确保同一个信号与槽函数之间只建立一次连接,避免重复连接。 |
💡 笔记总结 :
Qt::AutoConnection是默认的连接类型,它会根据信号与槽函数是否在同一线程自动选择合适的连接方式。跨线程通信时推荐使用Qt::QueuedConnection或Qt::BlockingQueuedConnection,其中后者会阻塞发送线程直到槽函数执行完毕,使用时需警惕死锁风险。
1.4 线程同步同步
在多线程编程中,当多个线程同时访问共享数据时,可能会出现数据竞争(Data Race)的问题。Qt 提供了多种线程同步机制来保证数据安全。
1.4.1 QMutex 互斥锁
QMutex 是最常用的同步机制,它保证同一时刻只有一个线程可以访问被保护的资源。
特点:QMutex 是 Qt 框架提供的互斥锁类,⽤于保护共享资源的访问,实现线程间的互斥操作。
⽤途:在多线程环境下,通过互斥锁来控制对共享数据的访问,确保线程安全。
cpp
// 示例:使用 QMutex 保护共享数据
QMutex mutex;
int sharedCounter = 0;
void WorkerThread::run()
{
for (int i = 0; i < 100; ++i) {
mutex.lock(); // 加锁
sharedCounter++; // 访问共享数据
mutex.unlock(); // 解锁
}
}
💡 笔记总结 :
lock()和unlock()必须成对使用。如果忘记解锁,会导致其他线程永久阻塞(死锁)。更安全的做法是使用QMutexLocker,它在作用域结束时自动解锁。
1.4.2 QMutexLocker 自动锁
特点:QMutexLocker 是 QMutex 的辅助类,使⽤ RAII(Resource Acquisition Is Initialization)⽅式对互斥锁进⾏上锁和解锁操作。
⽤途:简化对互斥锁的上锁和解锁操作,避免忘记解锁导致的死锁等问题。
cpp
// 使用 QMutexLocker 自动管理锁
void WorkerThread::run()
{
for (int i = 0; i < 100; ++i) {
QMutexLocker locker(&mutex); // 构造时自动加锁
sharedCounter++; // 访问共享数据
// 离开作用域时,locker 自动解锁
}
}
QReadWriteLocker、QReadLocker、QWriteLocker
特点:
QReadWriteLock 是读写锁类,⽤于控制读和写的并发访问。
QReadLocker ⽤于读操作上锁,允许多个线程同时读取共享资源。
QWriteLocker ⽤于写操作上锁,只允许⼀个线程写⼊共享资源。
⽤途:在某些情况下,多个线程可以同时读取共享数据,但只有⼀个线程能够进⾏写操作。读写锁提供了更⾼效的并发访问⽅式。
cpp
QReadWriteLock rwLock;
//在读操作中使⽤读锁
{
QReadLocker locker(&rwLock); //在作⽤域内⾃动上读锁
//读取共享资源
//...
} //在作⽤域结束时⾃动解读锁
//在写操作中使⽤写锁
{
QWriteLocker locker(&rwLock); //在作⽤域内⾃动上写锁
//修改共享资源
//...
} //在作⽤域结束时⾃动解写锁
⽰例1:
cpp
******************mainwindow.h*********************
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
cpp
********************mythread.h**********************
#ifndef MYTHREAD_H
#define MYTHREAD_H
#include<QThread>
#include<QMutex>
class mythread : public QThread
{
//如果需要使用跨线程的信号和槽机制,此处的宏必须添加,
//创建子线程类默认没有添加,需要手动添加,需要注意!!!
Q_OBJECT
public:
mythread();
void run();
private:
static QMutex mutex;//多个线程使用同一把锁
static int num;//多个线程访问同一个数据
};
#endif // MYTHREAD_H
cpp
*********************mainwindow.cpp***********************
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "mythread.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
mythread* t1 = new mythread();
mythread* t2 = new mythread();
t1->start();
t2->start();
}
MainWindow::~MainWindow()
{
delete ui;
}
cpp
***********************mythread.cpp**********************
#include "mythread.h"
#include<QDebug>
QMutex mythread::mutex;
int mythread::num = 0;
mythread::mythread()
{
}
void mythread::run()
{
while(true)
{
this->mutex.lock();//加锁
qDebug() << "Current Thread:" << this << ",Value:" << this->num++;
this->mutex.unlock();//解锁
QThread::sleep(1);//每隔1秒num加1
}
}

⽰例2:在上述⽰例的基础上使⽤ QMutexLocker 锁。
cpp
#include "mythread.h"
#include<QDebug>
QMutex mythread::mutex;
int mythread::num = 0;
mythread::mythread()
{
}
void mythread::run()
{
while(true)
{
// this->mutex.lock();//加锁
// qDebug() << "Current Thread:" << this << ",Value:" << this->num++;
// this->mutex.unlock();//解锁
// QThread::sleep(1);//每隔1秒num加1
//QMutexLocker:创建的时候加锁,当QMutexLocker局部销毁时解锁
{
QMutexLocker lock(&this->mutex);
qDebug() << "Current Thread:" << this << ",Value:" << this->num++;
}
}
}
效果同上!
1.4.3 QSemaphore 信号量
📌说明:信号量就是一个计数器 ,表示可用资源的 个数
QSemaphore 用于控制对有限数量资源的访问。它维护一个计数器,acquire() 减少计数,release() 增加计数。
信号量类似于增强的互斥锁,不仅能完成上锁和解锁操作,⽽且可以跟踪可⽤资源的数量。
特点 :QSemaphore 是 Qt 框架提供的计数信号量类,⽤于控制同时访问共享资源的线程数量。
⽤途:限制并发线程数量,⽤于解决⼀些资源有限的问题。
cpp
// 示例:信号量控制资源访问
QSemaphore semaphore(3); // 最多允许 3 个线程同时访问
void WorkerThread::run()
{
semaphore.acquire(); // 获取资源,计数减 1 - P操作
// 访问共享资源
semaphore.release(); // 释放资源,计数加 1 - V操作
}
1.4.4 QWaitCondition 条件变量
📌说明 :多个线程之间的调度是无序的,为了能够一定程度的干预线程之间的执行顺序,便引入了条件变量
QWaitCondition。
在多线程编程中,假设除了等待操作系统正在执⾏的线程之外,某个线程还必须等待某些条件满⾜才
能执⾏,这时就会出现问题 。这种情况下,线程会很⾃然地使⽤锁的机制来阻塞其他线程,因为这只
是线程的轮流使⽤,并且该线程等待某些特定条件,⼈们会认为需要等待条件的线程,在释放互斥锁
或读写锁之后进⼊了睡眠状态,这样其他线程就可以继续运⾏。当条件满⾜时,等待条件的线程将被
另⼀个线程唤醒。
在 Qt 中,专⻔提供了 QWaitCondition类 来解决像上述这样的问题。
特点 :QWaitCondition 是 Qt 框架提供的条件变量类,⽤于线程之间的消息通信和同步。
⽤途:在某个条件满⾜时等待或唤醒线程,⽤于线程的同步和协调。
cpp
// 示例:条件变量
QMutex mutex;
QWaitCondition condition;
bool dataReady = false;
// 生产者线程
void Producer::run()
{
// 生产数据
mutex.lock();
dataReady = true;
condition.wakeAll(); // 唤醒所有等待的线程
mutex.unlock();
}
// 消费者线程
void Consumer::run()
{
mutex.lock();
while (!dataReady) { //先判断线程继续执行的条件是否满足,不成立就进行wait等待
condition.wait(&mutex); // 等待条件满足
}
// 处理数据
mutex.unlock();
}
💡 笔记总结 :
wait()会先解锁互斥锁,然后阻塞等待;被唤醒后会重新加锁。使用while循环而不是if是为了防止"虚假唤醒"(spurious wakeup)。这里的while是为了判定唤醒之后,需要再确认一下,当前条件是否真的满足,因为wait可能会被提前唤醒(可能被信号打断了)。
1.5 线程间通信
Qt 中线程间通信最常用的方式是信号槽 。由于 QThread 继承自 QObject,因此可以定义信号,并在 run() 中发射信号,主线程通过槽函数接收。
cpp
// 线程类定义
class Worker : public QThread
{
Q_OBJECT
public:
void run() override;
signals:
void progress(int percent); // 进度信号
void finished(); // 完成信号
};
void Worker::run()
{
for (int i = 0; i <= 100; i += 10) {
emit progress(i); // 发射进度信号
msleep(100); // 模拟耗时操作
}
emit finished(); // 发射完成信号
}
💡 笔记总结 :信号槽是 Qt 多线程通信的核心。跨线程的信号槽连接默认使用队列连接(
QueuedConnection),信号发射后会在接收线程的事件循环中排队执行,因此是线程安全的。
1.6 线程池 QThreadPool
频繁创建和销毁线程会带来性能开销。QThreadPool 维护一组可复用的线程,配合 QRunnable 使用。
cpp
// 任务类
class Task : public QRunnable
{
public:
void run() override
{
// 执行任务
qDebug() << "任务执行中,线程ID:" << QThread::currentThreadId();
}
};
// 使用线程池
QThreadPool pool;
pool.setMaxThreadCount(4); // 设置最大线程数
Task *task = new Task();
pool.start(task); // 提交任务到线程池
💡 笔记总结 :
QThreadPool适合处理大量短小的任务,避免频繁创建线程的开销。QRunnable是任务接口,重写run()即可。
1.7 多线程注意事项
- 不要在子线程中直接操作 UI:Qt 的 UI 操作必须在主线程(GUI 线程)中执行。子线程中操作 UI 会导致程序崩溃或不可预知的行为。
- 使用信号槽进行线程通信:子线程通过信号通知主线程更新 UI,这是最安全的方式。
- 避免死锁:多个锁的加锁顺序要一致,避免交叉等待。
- 合理使用
terminate():terminate()会立即终止线程,可能导致资源未释放,应尽量避免使用,推荐通过标志位让线程自然退出。 - 线程安全 :访问共享数据时使用
QMutex、QReadWriteLock等同步机制。
💡 笔记总结 :子线程操作 UI 是初学者最容易犯的错误。记住一个原则:UI 操作永远在主线程,子线程只负责数据处理,通过信号把结果传给主线程。
1.8 总结
Qt 多线程编程的核心要点:
QThread是 Qt 多线程的基础类,通过继承并重写run()来实现线程逻辑。- 启动线程 使用
start(),不能直接调用run()。 - 线程通信通过信号槽机制,跨线程信号槽自动使用队列连接。
- 线程同步 使用
QMutex、QMutexLocker、QSemaphore、QWaitCondition等机制。 - 线程池
QThreadPool适合大量短小任务的场景。 - UI 操作必须在主线程,子线程通过信号通知主线程更新界面。
1.9 经典面试题
1. QThread 中 run() 和 start() 的区别是什么?
run()是线程的入口函数,定义了线程要执行的逻辑。start()是启动线程的函数,它会创建新的线程并调用run()。不能直接调用run(),否则它会在当前线程中执行,而不是在新线程中执行,失去了多线程的意义。
2. 为什么不能在子线程中直接操作 UI?
Qt 的 UI 系统不是线程安全的。所有 UI 操作必须在主线程(GUI 线程)中执行。如果在子线程中直接操作 UI,会导致程序崩溃或出现不可预知的行为。正确的做法是:子线程通过信号将数据发送给主线程,主线程的槽函数中更新 UI。
3. 什么是死锁?如何避免?
死锁是指两个或多个线程互相等待对方释放资源,导致所有线程都无法继续执行的状态。避免死锁的方法:① 加锁顺序保持一致;② 使用
QMutexLocker自动解锁;③ 避免嵌套锁;④ 使用超时锁(如tryLock(timeout))。
4. 信号槽跨线程连接时,是同步还是异步?
默认是异步的(队列连接
QueuedConnection)。信号发射后,槽函数会在接收线程的事件循环中排队执行,发射信号的线程不会等待槽函数执行完毕。如果需要同步执行,可以显式指定Qt::DirectConnection,但要注意线程安全问题。
5. 如何优雅地停止一个线程?
不要使用
terminate(),它会导致资源无法释放。推荐的做法:① 定义一个标志位(如bool m_stop);② 在run()的循环中检查标志位;③ 通过一个公共方法(如stop())设置标志位为true;④ 线程自然退出后,通过finished()信号进行清理。