目录
[一、Qt 多线程核心认知](#一、Qt 多线程核心认知)
[1.1 为什么需要多线程?](#1.1 为什么需要多线程?)
[1.2 Qt 多线程的优势](#1.2 Qt 多线程的优势)
[1.3 核心概念澄清](#1.3 核心概念澄清)
[二、QThread 核心 API 详解](#二、QThread 核心 API 详解)
[2.1 线程生命周期管理](#2.1 线程生命周期管理)
[2.2 线程休眠与当前线程](#2.2 线程休眠与当前线程)
[2.3 线程优先级](#2.3 线程优先级)
[2.4 信号与槽](#2.4 信号与槽)
[三、Qt 线程创建与使用:实战案例](#三、Qt 线程创建与使用:实战案例)
[3.1 基础案例:简单计时线程](#3.1 基础案例:简单计时线程)
[3.2 进阶案例:多线程数据共享与竞争](#3.2 进阶案例:多线程数据共享与竞争)
[步骤 1:线程竞争演示](#步骤 1:线程竞争演示)
[3.3 线程安全保障:互斥锁(QMutex)](#3.3 线程安全保障:互斥锁(QMutex))
[4.1 条件变量(QWaitCondition)](#4.1 条件变量(QWaitCondition))
[核心 API](#核心 API)
[实战案例:生产者 - 消费者模型](#实战案例:生产者 - 消费者模型)
[4.2 信号量(QSemaphore)](#4.2 信号量(QSemaphore))
[核心 API](#核心 API)
[4.3 读写锁(QReadWriteLock)](#4.3 读写锁(QReadWriteLock))
[核心 API](#核心 API)
[五、Qt 多线程常见问题与避坑指南](#五、Qt 多线程常见问题与避坑指南)
[5.1 子线程操作 UI 导致崩溃](#5.1 子线程操作 UI 导致崩溃)
[5.2 线程泄漏](#5.2 线程泄漏)
[5.3 死锁](#5.3 死锁)
[5.4 线程优先级滥用](#5.4 线程优先级滥用)
[5.5 信号槽连接类型选择](#5.5 信号槽连接类型选择)
[6.1 核心 API](#6.1 核心 API)
[6.2 实战案例:线程池处理批量任务](#6.2 实战案例:线程池处理批量任务)
前言
在 Qt 开发中,面对复杂计算、耗时 IO 操作(如文件下载、数据解析)时,单线程往往会导致界面卡顿、响应迟缓,严重影响用户体验。而多线程技术能让程序 "并行" 处理多个任务,在保证界面流畅的同时提升执行效率。Qt 封装了强大的
QThread类,提供了跨平台的多线程解决方案,无需关注底层操作系统的线程实现差异。本文将从基础概念出发,手把手教你掌握 Qt 多线程的使用技巧、核心 API、线程创建流程以及线程安全保障,让你轻松搞定并发编程!下面就让我们正式开始吧!
一、Qt 多线程核心认知
1.1 为什么需要多线程?
单线程程序的执行流程是线性的,所有任务按顺序依次执行。当遇到耗时操作(如大数据计算、网络请求、文件读写)时,主线程会被阻塞,导致界面无法响应鼠标点击、键盘输入等用户操作,出现 "假死" 现象。
多线程的核心价值在于任务并行执行:将耗时任务分配到子线程中处理,主线程专注于界面渲染和用户交互,从而实现 "界面流畅 + 任务高效执行" 的双赢。例如:
- 下载文件时,子线程负责接收数据,主线程实时显示下载进度;
- 数据分析时,子线程处理数据,主线程展示分析结果;
- 后台监控时,子线程循环采集数据,主线程更新监控面板。
1.2 Qt 多线程的优势
Qt 的**QThread**类封装了底层线程操作,相比原生 C++ 线程(std::thread),具有以下优势:
- 跨平台兼容:一套代码适配 Windows、Linux、macOS 等系统,Qt 自动处理底层线程 API 差异;
- 无缝集成 Qt 生态:支持信号槽机制,方便子线程与主线程通信;
- 完善的线程管理:提供线程启动、暂停、终止、休眠等完整功能;
- 线程安全保障 :配套提供
QMutex、QWaitCondition等同步工具,避免线程安全问题。
1.3 核心概念澄清
- 主线程:程序启动时默认创建的线程,负责界面渲染和用户交互,也称为 "UI 线程";
- 子线程:用户手动创建的线程,用于执行耗时任务,不能直接操作 UI 控件;
- 线程安全:多个线程同时访问共享资源时,不会出现数据错乱、崩溃等问题;
- 共享资源:多个线程共同访问的变量、数据结构、文件等(如全局变量、类成员变量);
- 同步机制:保证线程安全的手段(如互斥锁、条件变量、信号量)。
二、QThread 核心 API 详解
QThread是 Qt 多线程的核心类,封装了线程的创建、启动、管理等功能。以下是最常用的 API,按功能分类整理,方便快速查阅。
2.1 线程生命周期管理
| API 函数 | 功能说明 | 关键细节 |
|---|---|---|
| void start(Priority priority = InheritPriority) | 启动线程,调用run()函数 |
线程状态变为 "运行中",若已运行则无效果;priority指定线程优先级 |
| void terminate() | 强制终止线程 | 不推荐优先使用,可能导致资源泄漏,需配合wait()确保线程终止 |
| bool wait(unsigned long time = ULONG_MAX) | 阻塞当前线程,等待目标线程结束 | time为等待超时时间(毫秒),默认无限等待;返回true表示线程正常结束 |
| void exit(int returnCode = 0) | 退出线程事件循环 | returnCode为退出码,0 表示正常退出 |
| void quit() | 等价于exit(0),退出线程事件循环 |
仅对开启事件循环的线程有效(如使用exec()) |
| bool isRunning() const | 判断线程是否正在运行 | 返回true表示线程处于运行状态 |
| bool isFinished() const | 判断线程是否已结束 | 返回true表示线程已退出(run()函数执行完毕) |
2.2 线程休眠与当前线程
| API 函数 | 功能说明 | 注意事项 |
|---|---|---|
| static void sleep(unsigned long sec) | 使当前线程休眠指定秒数 | 静态函数,作用于当前执行线程 |
| static void msleep(unsigned long msec) | 使当前线程休眠指定毫秒数 | 精度依赖操作系统,可能存在微小误差 |
| static void usleep(unsigned long usec) | 使当前线程休眠指定微秒数 | 适用于高精度休眠场景 |
| *static QThread currentThread() | 返回当前执行线程的指针 | 常用于打印线程 ID、判断线程身份 |
| static Qt::HANDLE currentThreadId() | 返回当前线程的系统原生 ID | 用于与系统原生线程 API 交互 |
2.3 线程优先级
QThread::Priority枚举定义了线程优先级,影响操作系统的调度策略,常用值如下:
QThread::IdlePriority:最低优先级,仅当无其他线程运行时执行;QThread::LowestPriority:较低优先级;QThread::NormalPriority:默认优先级;QThread::HighestPriority:较高优先级;QThread::TimeCriticalPriority:最高优先级,尽量占用 CPU 资源。
2.4 信号与槽
QThread提供了关键信号,用于监听线程状态变化:
- void started():线程启动时触发(
run()函数执行前);- void finished():线程结束时触发(
run()函数执行完毕后);- void terminated():线程被
terminate()强制终止时触发。
三、Qt 线程创建与使用:实战案例
Qt 创建线程的核心流程是:自定义线程类继承QThread → 重写run()函数(线程入口) → 实例化线程对象 → 调用start()启动线程。下面通过多个实战案例,详解不同场景下的线程使用方法。
3.1 基础案例:简单计时线程
实现功能:子线程每隔 1 秒发送当前时间到主线程,主线程更新界面显示。
首先创建Qt项目,并编辑UI界面:

新建TimeThread类,继承QThread:

重写run()函数,通过信号发送时间数据:
timethread.h
cpp
#ifndef TIMETHREAD_H
#define TIMETHREAD_H
#include <QThread>
#include <QTime>
#include <QObject>
class TimeThread : public QThread
{
Q_OBJECT // 必须添加,支持信号槽
public:
explicit TimeThread(QObject *parent = nullptr);
protected:
// 线程入口函数,线程启动后自动执行
void run() override;
signals:
// 发送时间信号,主线程接收并更新UI
void sendTime(QString currentTime);
};
#endif // TIMETHREAD_H
timethread.cpp
cpp
#include "timethread.h"
#include <QDebug>
TimeThread::TimeThread(QObject *parent) : QThread(parent)
{
}
void TimeThread::run()
{
qDebug() << "子线程启动,线程ID:" << currentThreadId();
// 循环发送时间,每隔1秒一次
while (1)
{
// 获取当前时间,格式化为"hh:mm:ss"
QString timeStr = QTime::currentTime().toString("hh:mm:ss");
// 发送信号到主线程
emit sendTime(timeStr);
// 线程休眠1秒
sleep(1);
}
}
在主窗口(Widget)中实例化TimeThread对象,绑定信号槽,接收子线程发送的时间并更新界面。
widget.h
cpp
#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_startBtn_clicked();
// 接收子线程时间信号的槽函数
void showCurrentTime(QString timeStr);
private:
Ui::Widget *ui;
TimeThread *timeThread; // 线程对象指针
};
#endif // WIDGET_H
widget.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
this->setWindowTitle("Qt多线程计时示例");
// 实例化线程对象
timeThread = new TimeThread(this);
// 绑定子线程信号到主线程槽函数
// 注意:跨线程信号槽默认使用QueuedConnection,确保线程安全
connect(timeThread, &TimeThread::sendTime, this, &Widget::showCurrentTime);
qDebug() << "主线程ID:" << QThread::currentThreadId();
}
Widget::~Widget()
{
// 停止线程并等待结束,避免资源泄漏
timeThread->terminate();
timeThread->wait();
delete ui;
}
void Widget::on_startBtn_clicked()
{
if (!timeThread->isRunning())
{
timeThread->start(); // 启动线程
ui->startBtn->setText("线程已启动");
ui->startBtn->setEnabled(false);
}
}
void Widget::showCurrentTime(QString timeStr)
{
// 主线程更新UI
ui->timeLabel->setText(timeStr);
ui->timeLabel->setStyleSheet("font-size: 36px; text-align: center;");
}
运行效果
- 点击 "启动计时" 按钮,线程启动,
timeLabel每秒更新一次当前时间;- 主线程界面始终流畅,可正常拖拽、关闭;
- 控制台输出主线程和子线程的 ID,验证两者为不同线程。

关键说明
- **run()**函数是线程的入口,线程启动后自动执行,所有耗时操作应放在此处;
- 子线程不能直接操作 UI 控件 (如
ui->timeLabel->setText()),必须通过信号槽机制通知主线程更新;- 线程对象销毁前,需调用**terminate()强制终止(或让
run()函数正常退出),并调用wait()**等待线程结束,避免资源泄漏。
3.2 进阶案例:多线程数据共享与竞争
当多个线程同时访问共享资源(如全局变量、类成员变量)时,会出现 "线程竞争" 问题,导致数据错乱。下面通过案例演示线程竞争现象,再引入同步机制解决。
步骤 1:线程竞争演示
创建两个子线程,同时对一个全局变量num进行自增操作,观察数据错乱现象。
mythread.h
cpp
#ifndef MYTHREAD_H
#define MYTHREAD_H
#include <QThread>
#include <QMutex>
#include <QDebug>
class MyThread : public QThread
{
Q_OBJECT
public:
explicit MyThread(QObject *parent = nullptr);
protected:
void run() override;
private:
static int num; // 静态变量,多个线程共享
static QMutex mutex; // 静态互斥锁,保护共享资源
};
// 初始化静态变量
int MyThread::num = 0;
QMutex MyThread::mutex;
#endif // MYTHREAD_H
mythread.cpp
cpp
#include "mythread.h"
MyThread::MyThread(QObject *parent) : QThread(parent)
{
}
void MyThread::run()
{
while (1)
{
// 未加锁,存在线程竞争
num++;
qDebug() << "线程" << currentThreadId() << ":num = " << num;
msleep(500); // 休眠500毫秒,放大竞争概率
}
}
mainwindow.cpp(主线程启动两个子线程)
cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "mythread.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
this->setWindowTitle("线程竞争演示");
// 创建两个线程
MyThread *t1 = new MyThread(this);
MyThread *t2 = new MyThread(this);
// 启动线程
t1->start();
t2->start();
}
MainWindow::~MainWindow()
{
delete ui;
}
运行效果
控制台输出如下(数据错乱):
线程 0x7f8c3c000b80 :num = 1
线程 0x7f8c3b800000 :num = 2
线程 0x7f8c3c000b80 :num = 3
线程 0x7f8c3b800000 :num = 3 // 数据重复,出现竞争
线程 0x7f8c3c000b80 :num = 4
线程 0x7f8c3b800000 :num = 5
问题分析
**num++**看似是一个操作,实则包含三个步骤:
- 读取
num的当前值;- 将值加 1;
- 把新值写回
num。
当两个线程同时执行时,可能出现**"线程 A 读取值后,线程 B 也读取同一个值,两者加 1 后写回,导致num只增加 1"** 的情况,即线程竞争。
3.3 线程安全保障:互斥锁(QMutex)
为解决线程竞争,需使用互斥锁(QMutex) 保护共享资源。互斥锁保证同一时间只有一个线程能访问共享资源,其他线程需等待锁释放后才能访问。
修改mythread.cpp,在访问num前后添加锁操作:
cpp
void MyThread::run()
{
while (1)
{
mutex.lock(); // 上锁,若已上锁则阻塞等待
// 临界区:访问共享资源
num++;
qDebug() << "线程" << currentThreadId() << ":num = " << num;
mutex.unlock(); // 解锁,释放资源
msleep(500);
}
}
运行效果
控制台输出如下(数据正常递增):
线程 0x7f8c3c000b80 :num = 1
线程 0x7f8c3b800000 :num = 2
线程 0x7f8c3c000b80 :num = 3
线程 0x7f8c3b800000 :num = 4
线程 0x7f8c3c000b80 :num = 5
线程 0x7f8c3b800000 :num = 6
进阶:QMutexLocker(自动上锁解锁)
手动调用**lock()和unlock()**可能因异常、return 等导致解锁遗漏,引发死锁。QMutexLocker是QMutex的辅助类,采用 RAII(资源获取即初始化)机制,自动管理锁的生命周期:
- 构造时自动上锁;
- 析构时自动解锁(离开作用域时)。
修改mythread.cpp,使用QMutexLocker:
cpp
void MyThread::run()
{
while (1)
{
// 构造时自动上锁,作用域结束时自动解锁
QMutexLocker locker(&mutex);
num++;
qDebug() << "线程" << currentThreadId() << ":num = " << num;
msleep(500);
}
}
关键说明
- 互斥锁应只保护 "临界区"(访问共享资源的代码段),避免扩大锁的范围导致性能下降;
- 多个线程必须使用同一把互斥锁保护同一个共享资源,否则无法起到同步作用;
QMutexLocker是推荐用法,简化代码并避免死锁风险。
四、线程安全高级同步机制
除了互斥锁,Qt 还提供了条件变量、信号量、读写锁等高级同步机制,适用于不同场景。
4.1 条件变量(QWaitCondition)
条件变量用于线程间的 "通信",让一个线程等待某个条件满足后再继续执行。例如:线程 A 等待线程 B 完成数据准备后,再开始处理数据。
核心 API
- void wait(QMutex *mutex):释放互斥锁,让线程进入等待状态,直到被唤醒;
- void wakeOne():唤醒一个等待的线程;
- void wakeAll():唤醒所有等待的线程。
实战案例:生产者 - 消费者模型
- 生产者线程:生成数据,存入共享缓冲区;
- 消费者线程:等待缓冲区有数据后,取出并处理;
- 条件变量:缓冲区为空时,消费者等待;缓冲区有数据时,生产者唤醒消费者。
producerconsumer.h
cpp
#ifndef PRODUCERCONSUMER_H
#define PRODUCERCONSUMER_H
#include <QThread>
#include <QMutex>
#include <QWaitCondition>
#include <QList>
#include <QDebug>
// 共享缓冲区容量
#define BUFFER_SIZE 5
class Producer : public QThread
{
Q_OBJECT
public:
Producer(QMutex *mutex, QWaitCondition *cond, QList<int> *buffer, QObject *parent = nullptr);
void run() override;
private:
QMutex *m_mutex;
QWaitCondition *m_cond;
QList<int> *m_buffer;
};
class Consumer : public QThread
{
Q_OBJECT
public:
Consumer(QMutex *mutex, QWaitCondition *cond, QList<int> *buffer, QObject *parent = nullptr);
void run() override;
private:
QMutex *m_mutex;
QWaitCondition *m_cond;
QList<int> *m_buffer;
};
#endif // PRODUCERCONSUMER_H
producerconsumer.cpp
cpp
#include "producerconsumer.h"
// 生产者线程
Producer::Producer(QMutex *mutex, QWaitCondition *cond, QList<int> *buffer, QObject *parent)
: QThread(parent), m_mutex(mutex), m_cond(cond), m_buffer(buffer)
{
}
void Producer::run()
{
int data = 0;
while (1)
{
QMutexLocker locker(m_mutex);
// 缓冲区满,等待消费者取走数据
while (m_buffer->size() >= BUFFER_SIZE)
{
qDebug() << "缓冲区满,生产者等待...";
m_cond->wait(m_mutex); // 释放锁,进入等待
}
// 生产数据,存入缓冲区
data++;
m_buffer->append(data);
qDebug() << "生产者生产数据:" << data << ",缓冲区大小:" << m_buffer->size();
// 唤醒消费者(缓冲区有数据了)
m_cond->wakeOne();
msleep(1000); // 模拟生产耗时
}
}
// 消费者线程
Consumer::Consumer(QMutex *mutex, QWaitCondition *cond, QList<int> *buffer, QObject *parent)
: QThread(parent), m_mutex(mutex), m_cond(cond), m_buffer(buffer)
{
}
void Consumer::run()
{
while (1)
{
QMutexLocker locker(m_mutex);
// 缓冲区空,等待生产者生产数据
while (m_buffer->isEmpty())
{
qDebug() << "缓冲区空,消费者等待...";
m_cond->wait(m_mutex); // 释放锁,进入等待
}
// 消费数据,从缓冲区取出
int data = m_buffer->takeFirst();
qDebug() << "消费者消费数据:" << data << ",缓冲区大小:" << m_buffer->size();
// 唤醒生产者(缓冲区有空位了)
m_cond->wakeOne();
msleep(1500); // 模拟消费耗时
}
}
mainwindow.cpp(启动生产者和消费者线程)
cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "producerconsumer.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
this->setWindowTitle("生产者-消费者模型");
// 初始化共享资源和同步工具
QMutex *mutex = new QMutex;
QWaitCondition *cond = new QWaitCondition;
QList<int> *buffer = new QList<int>;
// 创建并启动线程
Producer *producer = new Producer(mutex, cond, buffer, this);
Consumer *consumer = new Consumer(mutex, cond, buffer, this);
producer->start();
consumer->start();
}
运行效果
缓冲区空,消费者等待...
生产者生产数据:1 ,缓冲区大小:1
消费者消费数据:1 ,缓冲区大小:0
生产者生产数据:2 ,缓冲区大小:1
消费者消费数据:2 ,缓冲区大小:0
生产者生产数据:3 ,缓冲区大小:1
生产者生产数据:4 ,缓冲区大小:2
消费者消费数据:3 ,缓冲区大小:1
生产者生产数据:5 ,缓冲区大小:2
消费者消费数据:4 ,缓冲区大小:1
生产者生产数据:6 ,缓冲区大小:2
缓冲区满,生产者等待...
消费者消费数据:5 ,缓冲区大小:1
生产者生产数据:7 ,缓冲区大小:2
关键说明
- 条件变量必须与互斥锁配合使用,**wait()**函数会自动释放锁,避免死锁;
- 使用
while循环判断条件(而非if),防止线程被虚假唤醒(操作系统层面的异常唤醒);- **wakeOne()**唤醒一个等待线程,**wakeAll()**唤醒所有等待线程,根据场景选择使用。
4.2 信号量(QSemaphore)
信号量是互斥锁的扩展,支持限制同时访问共享资源的线程数量(互斥锁仅允许一个线程访问)。适用于资源有限的场景(如有限的数据库连接、网络连接)。
核心 API
- QSemaphore(int n = 0):构造函数,
n为可用资源数量;- void acquire(int n = 1):获取
n个资源,若资源不足则阻塞;- void release(int n = 1):释放
n个资源;- int available() const:返回当前可用的资源数量。
实战案例:限制并发线程数
假设系统只有 2 个网络连接资源,创建 5 个线程,每个线程需要获取 1 个连接资源才能执行任务,执行完毕后释放资源。
semaphorethread.h
cpp
#ifndef SEMAPHORETHREAD_H
#define SEMAPHORETHREAD_H
#include <QThread>
#include <QSemaphore>
#include <QDebug>
class SemaphoreThread : public QThread
{
Q_OBJECT
public:
SemaphoreThread(QSemaphore *sem, int id, QObject *parent = nullptr);
void run() override;
private:
QSemaphore *m_sem;
int m_threadId;
};
#endif // SEMAPHORETHREAD_H
semaphorethread.cpp
cpp
#include "semaphorethread.h"
SemaphoreThread::SemaphoreThread(QSemaphore *sem, int id, QObject *parent)
: QThread(parent), m_sem(sem), m_threadId(id)
{
}
void SemaphoreThread::run()
{
qDebug() << "线程" << m_threadId << "尝试获取资源...";
m_sem->acquire(); // 获取1个资源,资源不足则阻塞
// 资源获取成功,执行任务
qDebug() << "线程" << m_threadId << "获取资源成功,开始执行任务...";
msleep(2000); // 模拟任务耗时
qDebug() << "线程" << m_threadId << "任务执行完毕,释放资源";
m_sem->release(); // 释放1个资源
}
mainwindow.cpp
cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "semaphorethread.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
this->setWindowTitle("信号量限制并发线程数");
// 信号量初始化,可用资源数量为2
QSemaphore *sem = new QSemaphore(2);
// 创建5个线程
for (int i = 1; i <= 5; ++i)
{
SemaphoreThread *thread = new SemaphoreThread(sem, i, this);
thread->start();
}
}
运行效果
线程 1 尝试获取资源...
线程 1 获取资源成功,开始执行任务...
线程 2 尝试获取资源...
线程 2 获取资源成功,开始执行任务...
线程 3 尝试获取资源...
线程 4 尝试获取资源...
线程 5 尝试获取资源...
线程 1 任务执行完毕,释放资源
线程 3 获取资源成功,开始执行任务...
线程 2 任务执行完毕,释放资源
线程 4 获取资源成功,开始执行任务...
线程 3 任务执行完毕,释放资源
线程 5 获取资源成功,开始执行任务...
线程 4 任务执行完毕,释放资源
线程 5 任务执行完毕,释放资源
关键说明
- 信号量的核心是 "资源计数",**acquire()**减少计数,**release()**增加计数;
- 当计数为 0 时,**acquire()**会阻塞,直到有线程释放资源;
- 适用于需要限制并发访问数量的场景,比互斥锁更灵活。
4.3 读写锁(QReadWriteLock)
读写锁是一种优化的互斥锁,区分 "读操作" 和 "写操作":
- 多个线程可同时进行读操作(共享锁);
- 仅允许一个线程进行写操作(排他锁);
- 写操作执行时,所有读操作需等待(写操作优先级高于读操作)。
适用于 "读多写少" 的场景(如配置文件读取、数据查询),能提升并发效率。
核心 API
- void lockForRead():上读锁,允许多个线程同时读;
- void lockForWrite():上写锁,仅允许一个线程写;
- void unlock():解锁;
- QReadLocker:读锁辅助类,自动上锁解锁;
- QWriteLocker:写锁辅助类,自动上锁解锁。
实战案例:读写锁优化数据访问
创建 3 个读线程和 1 个写线程,共享一个数据集合,读线程频繁查询数据,写线程定期更新数据。
rwlockthread.h
cpp
#ifndef RWLOCKTHREAD_H
#define RWLOCKTHREAD_H
#include <QThread>
#include <QReadWriteLock>
#include <QList>
#include <QDebug>
class ReadThread : public QThread
{
Q_OBJECT
public:
ReadThread(QReadWriteLock *rwLock, QList<int> *dataList, int id, QObject *parent = nullptr);
void run() override;
private:
QReadWriteLock *m_rwLock;
QList<int> *m_dataList;
int m_threadId;
};
class WriteThread : public QThread
{
Q_OBJECT
public:
WriteThread(QReadWriteLock *rwLock, QList<int> *dataList, QObject *parent = nullptr);
void run() override;
private:
QReadWriteLock *m_rwLock;
QList<int> *m_dataList;
};
#endif // RWLOCKTHREAD_H
rwlockthread.cpp
cpp
#include "rwlockthread.h"
// 读线程
ReadThread::ReadThread(QReadWriteLock *rwLock, QList<int> *dataList, int id, QObject *parent)
: QThread(parent), m_rwLock(rwLock), m_dataList(dataList), m_threadId(id)
{
}
void ReadThread::run()
{
while (1)
{
// 上读锁,自动解锁
QReadLocker locker(m_rwLock);
// 读操作:遍历数据集合
qDebug() << "读线程" << m_threadId << "开始读取,数据集合:" << *m_dataList;
msleep(1000); // 模拟读耗时
}
}
// 写线程
WriteThread::WriteThread(QReadWriteLock *rwLock, QList<int> *dataList, QObject *parent)
: QThread(parent), m_rwLock(rwLock), m_dataList(dataList)
{
}
void WriteThread::run()
{
int data = 0;
while (1)
{
// 上写锁,自动解锁
QWriteLocker locker(m_rwLock);
// 写操作:添加数据
data++;
m_dataList->append(data);
qDebug() << "写线程更新数据,添加:" << data << ",数据集合大小:" << m_dataList->size();
msleep(3000); // 模拟写耗时
}
}
mainwindow.cpp
cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "rwlockthread.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
this->setWindowTitle("读写锁实战");
// 初始化共享数据和读写锁
QReadWriteLock *rwLock = new QReadWriteLock;
QList<int> *dataList = new QList<int>;
// 创建3个读线程
for (int i = 1; i <= 3; ++i)
{
ReadThread *readThread = new ReadThread(rwLock, dataList, i, this);
readThread->start();
}
// 创建1个写线程
WriteThread *writeThread = new WriteThread(rwLock, dataList, this);
writeThread->start();
}
运行效果
读线程 1 开始读取,数据集合:()
读线程 2 开始读取,数据集合:()
读线程 3 开始读取,数据集合:()
写线程更新数据,添加:1 ,数据集合大小:1
读线程 1 开始读取,数据集合:(1)
读线程 2 开始读取,数据集合:(1)
读线程 3 开始读取,数据集合:(1)
读线程 1 开始读取,数据集合:(1)
读线程 2 开始读取,数据集合:(1)
读线程 3 开始读取,数据集合:(1)
写线程更新数据,添加:2 ,数据集合大小:2
关键说明
- 读线程之间不互斥,可同时读取,提升并发效率;
- 写线程执行时,所有读线程会阻塞,确保数据一致性;
QReadLocker和QWriteLocker简化锁管理,避免遗漏解锁。
五、Qt 多线程常见问题与避坑指南
5.1 子线程操作 UI 导致崩溃
问题 :子线程中直接调用**ui->label->setText()**等 UI 操作,程序崩溃。
原因:Qt 的 UI 控件不是线程安全的,仅允许主线程访问。
解决方案 :通过信号槽机制,子线程发送信号,主线程接收信号并更新 UI(跨线程信号槽默认使用QueuedConnection,自动切换到主线程执行)。
5.2 线程泄漏
问题:线程执行完毕后,资源未释放,导致内存泄漏。
解决方案:
- 线程结束后调用**deleteLater()**删除线程对象;
- 主线程退出前,调用**terminate()强制终止子线程,再调用wait()**等待线程结束;
- 避免创建过多临时线程,可使用**线程池(
QThreadPool)**复用线程。
5.3 死锁
问题:多个线程互相等待对方释放锁,导致程序卡住。
常见场景:
- 线程 A 持有锁 1,等待锁 2;线程 B 持有锁 2,等待锁 1;
- 线程未释放锁就退出,导致其他线程无法获取锁。解决方案:
- 统一锁的获取顺序(如所有线程先获取锁 1,再获取锁 2);
- 使用
QMutexLocker自动解锁,避免手动解锁遗漏;- 避免在持有锁时调用外部函数(可能引发锁的嵌套)。
5.4 线程优先级滥用
问题:过度设置高优先级线程,导致低优先级线程无法执行。
解决方案:
- 优先使用默认优先级(
NormalPriority);- 仅在必要时提高关键线程的优先级,避免多个高优先级线程抢占资源。
5.5 信号槽连接类型选择
跨线程信号槽的连接类型(Qt::ConnectionType)影响线程安全:
Qt::AutoConnection(默认):自动判断,同线程用DirectConnection,跨线程用QueuedConnection;Qt::DirectConnection:信号发送时立即执行槽函数,槽函数在发送线程执行(跨线程不安全);Qt::QueuedConnection:槽函数被放入接收线程的事件队列,稍后执行(跨线程安全);Qt::BlockingQueuedConnection:发送线程阻塞,直到槽函数执行完毕(可能导致死锁,需确保发送线程和接收线程不是同一线程)。
推荐用法 :跨线程信号槽使用默认的AutoConnection,或显式指定QueuedConnection。
六、线程池(QThreadPool):线程复用优化
创建和销毁线程会消耗系统资源,对于短期任务、大量任务(如网络请求、数据处理),使用线程池可复用线程,提升性能。Qt 的QThreadPool管理一组线程,自动分配任务,无需手动管理线程生命周期。
6.1 核心 API
- static QThreadPool *globalInstance():获取全局线程池实例;
- void start(QRunnable *runnable, int priority = 0):提交任务到线程池;
- void setMaxThreadCount(int maxThreadCount):设置线程池最大线程数;
- int maxThreadCount() const:获取最大线程数;
- bool waitForDone(unsigned long time = ULONG_MAX):等待所有任务执行完毕。
6.2 实战案例:线程池处理批量任务
创建 10 个任务,提交到线程池,线程池自动分配线程执行任务。
task.h (任务类,继承QRunnable)
cpp
#ifndef TASK_H
#define TASK_H
#include <QRunnable>
#include <QDebug>
#include <QThread>
class Task : public QRunnable
{
public:
Task(int taskId);
void run() override; // 任务执行函数
private:
int m_taskId;
};
#endif // TASK_H
task.cpp
cpp
#include "task.h"
Task::Task(int taskId) : m_taskId(taskId)
{
// 设置任务执行完毕后自动删除
setAutoDelete(true);
}
void Task::run()
{
qDebug() << "任务" << m_taskId << "开始执行,线程ID:" << QThread::currentThreadId();
// 模拟任务耗时
QThread::msleep(1000);
qDebug() << "任务" << m_taskId << "执行完毕";
}
mainwindow.cpp
cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "task.h"
#include <QThreadPool>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
this->setWindowTitle("Qt线程池实战");
// 获取全局线程池实例
QThreadPool *pool = QThreadPool::globalInstance();
// 设置最大线程数为3
pool->setMaxThreadCount(3);
qDebug() << "线程池最大线程数:" << pool->maxThreadCount();
// 提交10个任务到线程池
for (int i = 1; i <= 10; ++i)
{
Task *task = new Task(i);
pool->start(task); // 提交任务
}
// 等待所有任务执行完毕(可选)
pool->waitForDone();
qDebug() << "所有任务执行完毕";
}
运行效果
线程池最大线程数:3
任务 1 开始执行,线程ID: 0x7f8c3c000b80
任务 2 开始执行,线程ID: 0x7f8c3b800000
任务 3 开始执行,线程ID: 0x7f8c3b000000
任务 1 执行完毕
任务 4 开始执行,线程ID: 0x7f8c3c000b80
任务 2 执行完毕
任务 5 开始执行,线程ID: 0x7f8c3b800000
任务 3 执行完毕
任务 6 开始执行,线程ID: 0x7f8c3b000000
...
所有任务执行完毕
关键说明
- 任务类需继承**
QRunnable,重写run()**函数(任务执行入口);- **setAutoDelete(true)**设置任务执行完毕后自动删除,避免内存泄漏;
- 线程池自动复用线程,无需手动创建和销毁线程,适合大量短期任务;
- **setMaxThreadCount()**设置最大线程数,建议根据 CPU 核心数调整(如 CPU 核心数的 2 倍)。
总结
掌握 Qt 多线程技术,能让你轻松应对复杂的并发场景,开发出高效、流畅的跨平台应用。建议多动手实践,结合实际项目场景灵活运用不同的同步机制和线程管理方式。如果你有任何问题或需要进一步探讨高级场景,欢迎在评论区留言交流!
