【从零开始的Qt开发指南】(二十)Qt 多线程深度实战指南:从基础 API 到线程安全,带你实现高效并发应用


目录

​编辑

前言

[一、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))

运行效果

进阶:QMutexLocker(自动上锁解锁)

关键说明

四、线程安全高级同步机制

[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 信号槽连接类型选择)

六、线程池(QThreadPool):线程复用优化

[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 生态:支持信号槽机制,方便子线程与主线程通信;
  • 完善的线程管理:提供线程启动、暂停、终止、休眠等完整功能;
  • 线程安全保障 :配套提供QMutexQWaitCondition等同步工具,避免线程安全问题。

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++**看似是一个操作,实则包含三个步骤:

  1. 读取num的当前值;
  2. 将值加 1;
  3. 把新值写回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 等导致解锁遗漏,引发死锁。QMutexLockerQMutex的辅助类,采用 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

关键说明

  • 读线程之间不互斥,可同时读取,提升并发效率;
  • 写线程执行时,所有读线程会阻塞,确保数据一致性;
  • QReadLockerQWriteLocker简化锁管理,避免遗漏解锁。

五、Qt 多线程常见问题与避坑指南

5.1 子线程操作 UI 导致崩溃

问题 :子线程中直接调用**ui->label->setText()**等 UI 操作,程序崩溃。

原因:Qt 的 UI 控件不是线程安全的,仅允许主线程访问。

解决方案 :通过信号槽机制,子线程发送信号,主线程接收信号并更新 UI(跨线程信号槽默认使用QueuedConnection,自动切换到主线程执行)。

5.2 线程泄漏

问题:线程执行完毕后,资源未释放,导致内存泄漏。

解决方案

  • 线程结束后调用**deleteLater()**删除线程对象;
  • 主线程退出前,调用**terminate()强制终止子线程,再调用wait()**等待线程结束;
  • 避免创建过多临时线程,可使用**线程池(QThreadPool)**复用线程。

5.3 死锁

问题:多个线程互相等待对方释放锁,导致程序卡住。

常见场景

  1. 线程 A 持有锁 1,等待锁 2;线程 B 持有锁 2,等待锁 1;
  2. 线程未释放锁就退出,导致其他线程无法获取锁。解决方案
  • 统一锁的获取顺序(如所有线程先获取锁 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 多线程技术,能让你轻松应对复杂的并发场景,开发出高效、流畅的跨平台应用。建议多动手实践,结合实际项目场景灵活运用不同的同步机制和线程管理方式。如果你有任何问题或需要进一步探讨高级场景,欢迎在评论区留言交流!

相关推荐
爱喝水的鱼丶2 小时前
SAP-ABAP:SAP性能侦探:STAD事务码的深度解析与应用实战
开发语言·数据库·学习·sap·abap
while(1){yan}2 小时前
SpringAOP
java·开发语言·spring boot·spring·aop
专注于大数据技术栈2 小时前
java学习--Collection
java·开发语言·学习
hetao17338372 小时前
2026-01-09~12 hetao1733837 的刷题笔记
c++·笔记·算法
techdashen2 小时前
Go 1.18+ slice 扩容机制详解
开发语言·后端·golang
fqbqrr2 小时前
2601C++,模块导出分类
前端·c++
小李独爱秋2 小时前
计算机网络经典问题透视:端到端时延和时延抖动有什么区别?
运维·服务器·计算机网络·安全·web安全
froginwe112 小时前
R 包:全面解析与高效使用指南
开发语言
Arwen3032 小时前
如何消除APP、软件的不安全下载提示?怎样快速申请代码签名证书?
网络·网络协议·tcp/ip·安全·php·ssl