深入浅出之线程(Qt)

一、概述

“线程”(Thread)是计算机科学中的一个重要概念,特别是在多线程编程和多任务处理中。以下是对线程的详细解释:

1.1 定义

线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

1.2 特性

  1. 轻量级进程:线程是进程内的一条执行路径或执行单元,是进程的一个组成部分,一个进程可以有多个线程,它们共享进程的资源。
  2. 独立调度和分派的基本单位:在支持多线程的操作系统中,操作系统独立调度和分派(线程)是基本单位,线程基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
  3. 可并发执行:在一个进程中的多个线程之间可以并发执行,甚至允许在一个进程中所有线程都能并发执行(多处理器环境下)。
  4. 共享进程资源:由于线程属于进程,它拥有该进程的全部资源,包括打开的文件、创建的套接字等,而进程内的多个线程共享这些资源。

1.3 线程与进程的区别

  1. 资源占用:进程是资源分配的基本单位,拥有一个完整的虚拟地址空间,不依赖于线程而独立存在;而线程是CPU调度的基本单位,只拥有必不可少的资源,与同属一个进程的其他线程共享进程资源。
  2. 独立性:进程之间不能共享资源,但线程之间可以共享所在进程的资源。
  3. 系统开销:由于创建或撤销进程时,系统都要为之分配或回收资源,包括内存空间、页表项和内核数据结构等,操作系统所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程CPU环境的保存及新调度进程CPU环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。
  4. 通信方面:进程间通信(IPC)通常需要内核的支持。线程间可以直接读写同一进程地址空间中的共享存储区进行通信,但这需要谨慎处理同步问题。

二、Qt实现线程几种方式 

  多线程是提高应用程序性能和响应速度的常用技术之一,而在 Qt 中实现多线程也变得异常简单和高效。本文将对 Qt 中实现多线程的几种常用方法进行介绍,并结合示例程序展示其实际应用。

2.1  使用QThread类

QThread是Qt中用于处理线程的类。你可以通过继承QThread并重写其run()方法来定义线程的工作内容。然后,你可以创建该类的实例并调用start()方法来启动线程。

步骤如下:

1. 线程对象的创建

  • 首先,你需要创建一个QThread对象。这个对象将代表一个新的线程。

  • 你可以选择直接继承QThread并重写run方法,但通常推荐的做法是创建一个工作对象(通常是QObject的子类),然后将这个对象移动到QThread所代表的线程中。

2. 设置信号和槽

  • 在创建线程和工作对象之后,你需要设置信号和槽来协调主线程和工作线程之间的通信。

  • 例如,你可以设置一个信号来启动工作(通常连接到QThread::started信号),以及一个信号来指示工作完成(该信号可以触发QThread::quit方法的调用)。

3. 启动线程

  • 调用QThread对象的start方法来启动线程。这将导致QThreadrun方法(如果你重写了它)被调用,或者如果你使用了工作对象,则执行你移动到线程中的对象的槽函数。

4. 线程执行

  • 线程启动后,它将开始执行run方法中的代码,或者如果你使用了工作对象,则执行你连接到的槽函数中的代码。

  • run方法中,你可以包含任何需要在新线程中执行的任务。

5. 请求线程退出

  • run方法中的任务完成时,你需要确保线程能够正确地退出。

  • 如果你重写了run方法,你可以在任务完成后直接返回,这将导致线程退出。

  • 如果你使用了工作对象,则可以通过发送一个信号来触发QThread::quit方法的调用,然后等待线程完成其清理工作。

6. 等待线程结束(可选)

  • 在某些情况下,你可能需要等待线程完成其任务后再继续执行主线程中的代码。

  • 你可以使用QThread::wait方法来阻塞当前线程(通常是主线程),直到目标线程完成。

  • 注意:阻塞主线程可能会导致应用程序界面冻结,因此应谨慎使用。

7. 清理资源

  • 当线程完成后,你需要确保所有相关资源都被正确清理。

  • 这通常包括删除QThread对象和工作对象(如果你没有使用智能指针或父对象自动删除它们的话)。

  • 为了避免内存泄漏,你可以在适当的时机调用deleteLater方法,或者在确保线程已经退出后手动删除这些对象。

注意事项

  • 避免在run方法中执行阻塞操作:这可能会导致线程无法响应其他事件或信号。

  • 确保线程安全:多线程编程容易引入竞态条件和数据竞争。确保所有线程访问的共享数据都受到适当的同步机制(如互斥锁)的保护。

  • 正确管理生命周期:确保在适当的时候创建和销毁线程对象,以及正确地处理线程之间的通信和同步。

通过遵循这些步骤和注意事项,你可以有效地管理使用QThread类的run方法时线程的生命周期,并编写出健壮的多线程Qt应用程序。

#ifndef MYTHREAD_H
#define MYTHREAD_H

#include <QThread>
#include <QDebug>

class MyThread : public QThread
{
    Q_OBJECT
public:
    explicit MyThread(QObject *parent = nullptr);
    void run() override;
    void printTheadId();
};

#endif // MYTHREAD_H
#include "mythread.h"

MyThread::MyThread(QObject *parent)
    : QThread{parent}
{}

void MyThread::run()
{
    qDebug()<<"子线程ThreadID: "<<QThread::currentThreadId();
    // 线程的工作内容
    for (int i = 0; i < 3; ++i) {
        qDebug() << "Thread running:" << i;
        QThread::sleep(1); // 模拟耗时操作
    }
    printTheadId();
}

void MyThread::printTheadId()
{
    qDebug()<<"子线程ThreadID: "<<QThread::currentThreadId();
}
#include <QCoreApplication>
#include "mythread.h"
#include <QDebug>

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    // 在某处创建并启动线程
    MyThread *thread = new MyThread();
    thread->start();
    qDebug()<<"主线程ThreadID: "<<QThread::currentThreadId();
    thread->printTheadId();

    return a.exec();
}

运行结果: 

从上面结果可以看出:

  • 主线程和子线程执行的顺序不确定,偶尔主线程在前,偶尔子线程在前。
  • 子线程类的成员函数包括槽函数是运行在主线程当中的,只有run()函数运行在子线程中。
  • 如果在run()函数中调用子线程类成员函数,那么该成员函数运行在子线程中。

 分析原因:

QThread::run的作用

run 函数是做什么用的?Manual中说的清楚:

run 对于线程的作用相当于main函数对于应用程序。它是线程的入口,run的开始和结束意味着线程的开始和结束。run函数中的代码时确定无疑要在次线程中运行的。

注:重写run函数时使用override关键字的作用

在C++中,override关键字是一个上下文关键字,用于明确表示一个成员函数覆盖了基类中的虚函数。当你在派生类中重写一个从基类继承来的虚函数时,使用override关键字可以帮助编译器检查该函数签名是否与基类中的虚函数签名完全匹配。

使用override关键字的好处包括:

  1. 编译时检查:如果派生类中的函数签名与基类中的虚函数签名不匹配(例如,参数类型、数量或返回类型不同),编译器将报错。这有助于避免由于拼写错误或误解基类接口而导致的错误。

  2. 代码可读性override关键字使代码更易于理解,因为它清楚地表明该函数是故意重写基类中的虚函数的。这有助于其他开发人员(或未来的你)更快地理解代码的意图。

  3. 防止意外隐藏:如果没有使用override关键字,并且派生类中的函数签名与基类中的某个非虚函数签名相同,那么派生类中的函数将隐藏基类中的函数,而不是重写它。这可能导致难以调试的错误。使用override关键字可以避免这种情况,因为编译器会报错,指出没有函数被重写。

  4. 增强代码的可维护性:如果基类中的虚函数签名发生变化(例如,添加了新的参数或更改了返回类型),并且派生类中的相应函数使用了override关键字,那么编译器将报错,提示需要更新派生类中的函数签名以匹配基类中的新签名。这有助于确保派生类与基类的接口保持一致。

void run() override中,override关键字表示run函数是重写基类(很可能是QThread或某个其他包含虚run函数的类)中的虚run函数的。如果基类中没有名为run的虚函数,或者基类中的run函数签名与派生类中的run函数签名不匹配,编译器将报错。

 2.2 使用QObject和moveToThread

这种方法不直接继承QThread,而是创建一个普通的QObject子类,并将其实例移动到一个新线程中。然后,你可以使用信号和槽机制来在主线程和该对象之间进行通信。

步骤如下:

1. 创建和初始化

  • 创建QThread对象

你可以通过创建一个QThread对象来启动一个新线程。

QThread* thread = new QThread;
  • 设置线程的工作对象

通常,你不会直接继承QThread并重写run方法,而是创建一个包含工作逻辑的对象,并将其移动到新线程中。这可以避免一些继承相关的问题。

Worker* worker = new Worker;  
worker->moveToThread(thread);
  • 连接信号和槽

你需要连接适当的信号和槽,以便在主线程和工作线程之间通信。 

QObject::connect(thread, &QThread::started, worker, &Worker::doWork);  
QObject::connect(worker, &Worker::finished, thread, &QThread::quit);  
QObject::connect(worker, &Worker::finished, worker, &Worker::deleteLater);  
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);

2. 启动线程

  • 调用start方法

调用QThread对象的start方法来启动线程。这将调用QThread::run方法(如果你重写了它)或者执行你移动到线程中的对象的槽函数。

thread->start();

3. 线程运行

  • 执行工作
    线程启动后,将执行Worker对象的doWork槽函数(或其他你连接到的槽函数)。

  • 请求线程停止
    当工作完成时,你需要请求线程停止。这通常是通过发送一个信号给线程,该信号触发QThread::quit方法的调用。

4. 请求线程退出

  • 等待线程完成

你可以使用QThread::wait方法来阻塞当前线程,直到目标线程完成。注意,阻塞主线程可能会导致应用界面冻结。

emit worker->finished();

5. 等待线程结束

  • 等待线程完成
    你可以使用QThread::wait方法来阻塞当前线程,直到目标线程完成。注意,阻塞主线程可能会导致应用界面冻结。

thread->wait();

6. 清理资源

  • 删除线程对象
    当线程完成后,应该删除QThread对象和任何相关的工作对象。通常,这些对象的删除是通过deleteLater槽在适当的时机进行的。

注意事项

  • 避免直接继承QThread
    直接继承QThread并重写run方法通常不是最佳实践。更好的做法是使用QObject的子类,并将其实例移动到线程中。

  • 确保线程安全
    多线程编程容易引入竞态条件和数据竞争。确保所有线程访问的共享数据都受到适当的同步机制(如互斥锁)的保护。

  • 正确管理生命周期
    确保在适当的时候删除线程和工作对象,避免内存泄漏。

通过这些步骤,你可以有效地管理QThread类线程的生命周期,编写出健壮的多线程Qt应用程序。

#ifndef MYWORKER_H
#define MYWORKER_H

#include <QObject>
#include <QThread>
#include <QDebug>

class myWorker : public QObject
{
    Q_OBJECT
public:
    explicit myWorker(QObject *parent = nullptr);

    void printThreadId();

public slots:
    void doWork1();
    void doWork2();
    void doWork3();

signals:
    void testdoWork3();

};

#endif // MYWORKER_H
#include "mythread.h"

MyThread::MyThread(QObject *parent)
    : QThread{parent}
{}

void MyThread::run()
{
    qDebug()<<"子线程ThreadID: "<<QThread::currentThreadId();
    // 线程的工作内容
    for (int i = 0; i < 3; ++i) {
        qDebug() << "Thread running:" << i;
        QThread::sleep(1); // 模拟耗时操作
    }
    printThreadId();
}

void MyThread::printThreadId()
{
    qDebug()<<"子线程ThreadID: "<<QThread::currentThreadId();
}
#include <QCoreApplication>
#include <QObject>
#include "mythread.h"
#include "myworker.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);


    // 在某处创建线程和Worker对象
    QThread *thread = new QThread();
    myWorker *worker = new myWorker();

    // 将Worker对象移动到新线程中
    worker->moveToThread(thread);

    // 连接信号和槽
    QObject::connect(thread, &QThread::started, worker, &myWorker::doWork1);
    QObject::connect(thread, &QThread::started, worker, &myWorker::doWork2);
    QObject::connect(worker, &myWorker::testdoWork3, worker, &myWorker::doWork3); // 假设YourClass是主线程中的一个类
    //connect(worker, &myWorker::finished, thread, &QThread::quit); // 当Worker完成工作时,让线程退出
    //connect(worker, &myWorker::finished, worker, &myWorker::deleteLater); // 删除Worker对象
    QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); // 删除线程对象

    // 启动线程
    thread->start();

    qDebug()<<"主线程ThreadID: "<<QThread::currentThreadId();
    //调用成员数
    worker->printThreadId();

    //发送自定义信号
    emit worker->testdoWork3();


    return a.exec();
}

运行结果:

从上面运行结果可以看出:

  • 槽函数无论是线程的信号触发还是自定义信号触发,槽函数都在新线程里运行。
  • 成员函数和主函数运行在主线程当中。

2.3 继承QThread类和使用QObject有什么区别

继承QThread类和使用QObject(通常结合moveToThread方法)在Qt中实现多线程编程时,存在显著的区别。以下是对这两种方式的详细比较:

1、继承QThread

  1. 实现方式

    • 通过继承QThread类并重写其run()方法,将需要在线程中执行的代码放入run()方法中。

    • 线程的执行逻辑与QThread对象紧密绑定。

  2. 线程与对象的关联

    • QThread对象本身在新线程中执行run()方法。

    • QThread类的其他非run()函数(如槽函数)默认在主线程中执行,除非通过特殊手段(如moveToThread)改变其执行线程。

  3. 资源管理

    • 需要自己管理线程的创建、启动、停止和销毁。

    • 需要注意线程同步和资源共享问题,以避免数据竞争和未定义行为。

  4. 适用场景

    • 适用于实现相对简单独立运行的常驻或后台任务。

    • 适用于不需要跨线程频繁通信的任务。

2、使用QObject(结合moveToThread

  1. 实现方式

    • 创建一个继承自QObject的类,并在其中定义槽函数来执行实际工作。

    • 创建一个QThread对象,并使用moveToThread方法将QObject派生类的对象移动到该线程中。

    • 通过信号和槽机制在主线程中触发工作对象槽函数的执行。

  2. 线程与对象的关联

    • QObject派生类的对象在新线程中执行其槽函数。

    • 对象的创建和销毁由Qt的事件循环管理,无需手动删除(除非在特殊情况下)。

  3. 资源管理

    • 线程的管理相对简单,因为QObject的生命周期和线程关联由Qt自动处理。

    • 仍然需要注意线程同步和资源共享问题,但可以通过信号和槽机制实现线程间的安全通信。

  4. 适用场景

    • 适用于线程间需要频繁通信的任务。

    • 适用于需要将多个任务分配到不同线程中执行,并保持这些任务之间的数据共享和通信的场景。

3、总结

  • 灵活性:使用QObject结合moveToThread方法更加灵活,因为可以将不同的任务对象分配到不同的线程中,而无需为每个任务创建一个新的QThread子类。

  • 代码复用性:继承QThread类的方式中,线程逻辑与线程对象紧密绑定,不利于代码复用。而使用QObject的方式中,可以将工作逻辑封装在独立的QObject派生类中,更容易实现代码的复用和模块化。

  • 通信机制:继承QThread类时,如果需要跨线程通信,可能需要使用额外的同步机制(如互斥锁、条件变量等)。而使用QObject结合信号和槽机制时,可以更容易地实现线程间的安全通信。

综上所述,在选择实现方式时,应根据具体的应用场景和需求来决定。如果任务相对简单且不需要跨线程频繁通信,可以选择继承QThread类。如果任务复杂且需要跨线程通信和数据共享,则使用QObject结合moveToThread方法可能更为合适。

2.4 注意事项

  • 不要在子线程中直接操作GUI控件。如果需要更新GUI,请使用信号和槽机制将更新请求发送到主线程。
  • 谨慎处理线程间的共享数据。如果需要,使用Qt提供的同步工具(如QMutex)来保护共享资源。
  • 在设计多线程程序时,要考虑到线程的生命周期和资源管理,以避免内存泄漏和未定义行为。

 三、线程池

Qt线程池是Qt框架中用于管理线程资源、提高程序性能和效率的重要机制。以下是对Qt线程池的详细介绍:

3.1、线程池的基本概念

线程池是一种线程使用模式,它管理着一组可重用的线程,这些线程可以处理分配过来的可并发执行的任务。线程池设有最大线程数限制,以避免线程数过多导致的额外线程切换开销。通过重用线程,线程池可以减少创建和销毁线程的次数,从而提高程序的效率和性能。

3.2、Qt线程池的实现方式

Qt提供了多种实现线程池的方式,其中主要包括使用QThreadPool类、QtConcurrent库以及自定义线程池。

  1. QThreadPool类

    • QThreadPool是Qt中用于管理线程池的类。每个Qt应用程序都有一个全局QThreadPool对象,可以通过调用globalInstance()来访问。
    • 要想让线程池执行任务,需要创建一个QRunnable的子类并实现其run()虚函数,然后将这个子类的对象传递到QThreadPool的start()方法。
    • QThreadPool会自动管理线程的创建、销毁和任务的调度,开发者只需关注任务的实现和提交。
  2. QtConcurrent库

    • QtConcurrent是一个更高层次的并行编程框架,它简化了多线程编程的复杂性。
    • 使用QtConcurrent,开发者可以方便地实现并行执行任务,而无需直接操作线程和线程池。
    • QtConcurrent内部使用线程池来管理线程资源,并自动调度任务给空闲的线程执行。
  3. 自定义线程池

    • 在某些特殊场景下,开发者可能需要实现自定义的线程池以满足特定需求。
    • 自定义线程池通常涉及继承QThread类并实现其run()函数,然后在run()函数中循环等待任务的到来并执行任务。
    • 自定义线程池需要手动管理线程的创建、销毁和任务的调度,因此相对较为复杂。

3.3、QThreadPool和QRunnable实现线程池

以下是一个使用QThreadPool和QRunnable实现线程池的简单示例:

#ifndef MYTHREADPOOL_H
#define MYTHREADPOOL_H
#include <QObject>
#include <QThread>
#include <QRunnable >
#include <QDebug>

class MyThreadPool : public QObject, public QRunnable
{
    Q_OBJECT
public:
    MyThreadPool();
    virtual void run() override;
};

#endif // MYTHREADPOOL_H
#include "mythreadpool.h"

MyThreadPool::MyThreadPool() {}

void MyThreadPool::run()
{
    QThread::msleep(500);
    qDebug() << "aaa " << QThread::currentThreadId();
}
#include <QCoreApplication>
#include <QObject>
#include <QThreadPool>
#include "mythread.h"
#include "myworker.h"
#include "mythreadpool.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);


    auto pool = QThreadPool::globalInstance();
    pool->setMaxThreadCount(3); // 设置线程池最多3个线程

    MyThreadPool *printer = new MyThreadPool;
    printer->setAutoDelete(false); // 执行后不自动释放,因为要重复使用

    /* 打印十次 */
    for (int i = 0; i < 10; i++) {
        pool->start(printer);
    }

    pool->waitForDone(); // 等待执行完毕
    delete printer;

    return a.exec();
}

 在这个示例中,我们创建了一个继承自QRunnable的MyThreadPool类,并在其run()函数中打印了当前线程的ID。然后,我们获取全局QThreadPool对象,并设置最大线程数为3。接着,我们创建了一个PrintA对象,并将其传递给QThreadPool的start()方法以执行任务。最后,我们调用waitForDone()方法等待所有任务执行完毕,并手动删除PrintA对象。

运行结果:

3.4、QtConcurrent实现线程池

QtConcurrent 是 Qt 框架中的一个高级并行编程接口,它允许你以简洁的方式编写并行代码,而无需直接处理线程和线程池的细节。QtConcurrent 内部使用 Qt 的线程池来管理线程资源,并自动调度任务给空闲的线程执行。

以下是一个使用 QtConcurrent 实现并行计算的 C++ 示例。在这个示例中,我们将使用 QtConcurrent::run 来并行执行一个简单的计算任务,并收集结果。

首先,确保你的项目文件(.pro 文件)包含了 concurrent 模块:

QT += concurrent

然后,编写你的 C++ 代码:

#include <QCoreApplication>  
#include <QtConcurrent/QtConcurrent>  
#include <QFuture>  
#include <QFutureWatcher>  
#include <QDebug>  
#include <vector>  
  
// 一个简单的计算函数,它接受一个整数并返回其平方  
int computeSquare(int number) {  
    return number * number;  
}  
  
int main(int argc, char *argv[]) {  
    QCoreApplication a(argc, argv);  
  
    // 创建一个整数向量,包含我们想要计算平方的数  
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};  
  
    // 使用 QtConcurrent::mapped 来并行计算每个数的平方  
    // QtConcurrent::mapped 接受一个容器和一个函数,并对容器中的每个元素应用该函数  
    QFuture<int> future = QtConcurrent::mapped(numbers.begin(), numbers.end(), computeSquare);  
  
    // 创建一个 QFutureWatcher 来监视 future 的状态  
    QFutureWatcher<int> watcher;  
    QObject::connect(&watcher, &QFutureWatcher<int>::finished, [&]() {  
        // 当计算完成时,打印结果  
        QList<int> results = future.results();  
        for (int result : results) {  
            qDebug() << result;  
        }  
        QCoreApplication::quit(); // 退出应用程序  
    });  
  
    // 设置 QFutureWatcher 监视我们的 future  
    watcher.setFuture(future);  
  
    // 进入事件循环,等待计算完成  
    return a.exec();  
}

在这个示例中,我们定义了一个简单的 computeSquare 函数,它接受一个整数并返回其平方。然后,我们创建了一个包含整数的向量 numbers。使用 QtConcurrent::mapped 函数,我们可以并行地对向量中的每个元素应用 computeSquare 函数。

QtConcurrent::mapped 返回一个 QFuture<int> 对象,它表示异步计算的结果。我们创建了一个 QFutureWatcher<int> 对象来监视这个 QFuture 对象的状态。当计算完成时,QFutureWatcher 会发出 finished 信号,我们在信号处理器中打印结果并退出应用程序。

注意,在这个示例中,我们并没有直接创建或管理线程池。QtConcurrent 内部会自动处理这些细节,并使用全局的 QThreadPool 来执行并行任务。

编译并运行这个程序,你应该会看到 1 到 10 的平方被并行计算并打印出来(尽管由于线程调度的原因,输出的顺序可能会有所不同)。

运行结果:

3.5、Qt线程池的优点和注意事项

  1. 优点

    • 减少了线程的创建和销毁次数,降低了资源开销。
    • 自动调度和分配任务给空闲线程,提高了任务处理速度。
    • 通过限制最大线程数,可以控制并发执行的任务数量,避免资源竞争和过度消耗系统资源。
    • 简化了多线程编程的复杂性,开发者只需关注任务的实现和提交。
  2. 注意事项

    • 需要合理设置线程池的大小,以充分利用系统资源并避免资源竞争。
    • 当使用QThreadPool的默认自动删除功能时,要注意任务对象的生命周期管理,避免在任务执行完毕后立即删除任务对象而导致访问空指针。
    • 在多线程编程中,要注意线程安全和同步问题,避免数据竞争和死锁等问题的发生。

综上所述,Qt线程池是Qt框架中用于提高程序性能和效率的重要机制。通过合理使用线程池,开发者可以简化多线程编程的复杂性,并充分利用系统资源来处理并发任务。

四、线程的同步与互斥

在Qt中,线程的同步与互斥是确保多线程程序正确运行的关键机制。以下是对Qt线程同步与互斥的详细解释:

4.1、线程同步

线程同步是指通过一定的机制,使得多个线程能够按照特定的顺序或条件来访问共享资源或执行特定的代码段。Qt提供了多种线程同步机制,包括互斥锁(QMutex)、读写锁(QReadWriteLock)、条件变量(QWaitCondition)等。

  1. 互斥锁(QMutex)

    • 互斥锁用于保护共享资源,确保在同一时间只有一个线程能够访问该资源。
    • 线程在访问共享资源之前需要获取互斥锁,使用完后再释放互斥锁。
    • QMutex类提供了lock()和unlock()方法来获取和释放互斥锁。
  2. 读写锁(QReadWriteLock)

    • 读写锁是一种更高效的线程同步机制,它允许多个线程同时进行读操作,但只允许一个线程进行写操作。
    • QReadWriteLock类提供了lockForRead()和lockForWrite()方法来获取读锁和写锁,以及unlock()方法来释放锁。
  3. 条件变量(QWaitCondition)

    • 条件变量用于线程间的协调与等待/唤醒机制。
    • 线程可以在条件不满足时等待,并在条件满足时被其他线程唤醒。
    • QWaitCondition通常与QMutex一起使用,以确保条件的读写操作是线程安全的。

4.2、线程互斥

线程互斥是指通过一定的机制,防止多个线程同时访问共享资源或执行特定的代码段,从而避免数据竞争和不一致的问题。在Qt中,线程互斥主要通过互斥锁(QMutex)来实现。

  1. QMutex的使用

    • 在需要进行线程同步的地方,首先创建一个QMutex对象。
    • 线程在访问共享资源之前,使用QMutex的lock()方法获取互斥锁。
    • 如果互斥锁已被其他线程占用,当前线程会被阻塞,直到互斥锁被释放。
    • 线程完成对共享资源的访问后,使用unlock()方法释放互斥锁。
  2. QMutexLocker的辅助

    • QMutexLocker是QMutex的一个辅助类,它采用了资源获取即初始化(RAII)的设计模式。
    • 在QMutexLocker对象的生命周期内,互斥锁会被自动上锁,并在该对象被销毁时自动解锁。
    • 这极大地简化了对互斥锁的管理,避免了因忘记解锁而导致的死锁等潜在问题。

4.3、使用QMutex实现线程同步与互斥

在Qt中,QMutex(互斥锁)是用于线程同步与互斥的一种基本机制。它允许一个线程独占访问共享资源,而其他线程必须等待,直到该线程释放互斥锁。以下是一个使用QMutex进行线程同步与互斥的示例代码。

这个示例创建了两个线程,它们共享一个整数变量。一个线程负责增加这个变量的值,而另一个线程负责打印它的值。为了确保在增加和打印操作之间不会发生数据竞争,我们使用了QMutex来同步对共享变量的访问。

#include <QCoreApplication>
#include <QObject>
#include <QThread>
#include <QMutex>
#include <QDebug>

// 共享变量和互斥锁
int sharedValue = 0;
QMutex mutex;

// 增加共享变量值的线程类
class IncrementThread : public QThread
{
public:
    void run() override
    {
        for (int i = 0; i < 10; ++i) {
            // 加锁以保护对共享变量的访问
            mutex.lock();
            ++sharedValue;
            qDebug() << "Incremented sharedValue to" << sharedValue;
            // 解锁以允许其他线程访问共享变量
            // 模拟一些工作负载
            msleep(2); // 睡眠1秒
            mutex.unlock();


        }
    }
};

// 打印共享变量值的线程类
class PrintThread : public QThread
{
public:
    void run() override
    {
        for (int i = 0; i < 10; ++i) {
            // 加锁以保护对共享变量的访问
            mutex.lock();
            qDebug() << "PrintThread sharedValue is" << sharedValue;
            // 解锁以允许其他线程访问共享变量
            // 模拟一些工作负载
            msleep(1); // 睡眠1秒
            mutex.unlock();


        }
    }
};

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    IncrementThread incrementThread;
    PrintThread printThread;

    // 启动线程
    incrementThread.start();
    printThread.start();

    // 等待线程完成
    incrementThread.wait();
    printThread.wait();

    return a.exec();
}

注意

  1. 在这个示例中,IncrementThread 和 PrintThread 都频繁地锁定和解锁互斥锁,这可能导致性能下降,特别是在高并发场景下。在实际应用中,你应该尽量减少锁的持有时间,并考虑使用其他同步机制(如读写锁、条件变量等)来优化性能。

  2. 当你使用Qt的信号和槽机制时,通常不需要手动锁定和解锁互斥锁,因为Qt的信号和槽机制本身已经是线程安全的。但是,当你直接访问共享资源时(例如在run()方法中),你需要手动管理互斥锁。

  3. 在这个示例中,我们直接在main()函数中包含了main.moc,这是为了简化示例。在实际项目中,你应该确保你的.pro文件包含了正确的QT += coreCONFIG += c++11(或更高版本的C++标准)设置,并且你的源文件(例如main.cpp)被正确地添加到项目中。然后,qmake和make工具将自动生成和链接必要的MOC代码。

打印结果: 

 

4.4 使用QMutexLocker实现线程同步与互斥 

在Qt中,QMutexLocker 是一个辅助类,它简化了 QMutex 的使用。QMutexLocker 采用了资源获取即初始化(RAII)的设计模式,这意味着它在构造时自动获取互斥锁,并在析构时自动释放互斥锁。这种方式确保了即使在发生异常的情况下,互斥锁也能被正确释放,从而避免了死锁的风险。

以下是一个使用 QMutexLocker 进行线程同步与互斥的示例代码。这个示例创建了两个线程,它们共享一个整数变量,并使用 QMutex 和 QMutexLocker 来保护对该变量的访问。

#include <QCoreApplication>
#include <QThread>
#include <QMutex>
#include <QMutexLocker>
#include <QDebug>

// 共享变量和互斥锁
int sharedValue = 0;
QMutex mutex;

// 增加共享变量值的线程类
class IncrementThread : public QThread
{
public:
    void run() override
    {
        for (int i = 0; i < 10; ++i) {
            QMutexLocker locker(&mutex); // 自动加锁
            ++sharedValue;
            qDebug() << "Incremented sharedValue to" << sharedValue;
            // QMutexLocker 在离开作用域时自动解锁

            // 模拟一些工作负载
            msleep(1); // 睡眠1秒
        }
    }
};

// 打印共享变量值的线程类
class PrintThread : public QThread
{
public:
    void run() override
    {
        for (int i = 0; i < 10; ++i) {
            QMutexLocker locker(&mutex); // 自动加锁
            qDebug() << "Current sharedValue is" << sharedValue;
            // QMutexLocker 在离开作用域时自动解锁

            // 模拟一些工作负载
            msleep(1); // 睡眠1秒
        }
    }
};

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    IncrementThread incrementThread;
    PrintThread printThread;

    // 启动线程
    incrementThread.start();
    printThread.start();

    // 等待线程完成
    incrementThread.wait();
    printThread.wait();

    return a.exec();
}

注意

  1. 在这个示例中,QMutexLocker 对象在构造时自动获取了 mutex 互斥锁,并在其析构时(即离开作用域时)自动释放了锁。这确保了即使在发生异常的情况下,锁也能被正确释放。

  2. 由于 QMutexLocker 的存在,你不再需要手动调用 mutex.unlock()。当 QMutexLocker 对象被销毁时(例如,当它的作用域结束时),它会自动调用 mutex.unlock()

  3. 在实际项目中,确保你的 .pro 文件包含了正确的 QT += core 设置,并且你的源文件被正确地添加到项目中。然后,qmake 和 make 工具将自动生成和链接必要的 MOC 代码。在这个示例中,由于我们直接在 main() 函数中包含了 main.moc,这通常是不必要的,除非你在没有使用 qmake 的情况下手动编译代码。然而,在大多数情况下,你应该让 qmake 和构建系统来处理这些细节。

 运行结果:

4.5 使用QSemaphorer实现线程同步与互斥

在Qt中,QSemaphore 是一种线程同步机制,它允许线程在继续执行之前等待或计数信号量。与 QMutex 不同,QSemaphore 不提供互斥锁的功能(即它不会阻止其他线程访问共享资源),而是用于控制对资源的访问数量。信号量的值表示可用资源的数量,当值大于0时,线程可以递减信号量并继续执行;当值为0时,线程必须等待,直到其他线程释放资源并增加信号量。

以下是一个使用 QSemaphore 进行线程同步的示例代码。在这个示例中,我们创建了一个资源池(用整数表示资源的数量),并使用 QSemaphore 来控制对资源的访问。两个线程将尝试获取资源,并在使用完资源后释放它。

#include <QCoreApplication>
#include <QThread>
#include <QSemaphore>
#include <QDebug>

// 资源池的大小和信号量
const int ResourcePoolSize = 5;
QSemaphore semaphore(ResourcePoolSize); // 初始化为5,表示有5个可用资源

// 模拟使用资源的线程类
class ResourceThread : public QThread
{
    int resourceId; // 线程将尝试获取的资源ID(仅用于调试输出)

public:
    ResourceThread(int id) : resourceId(id) {}

protected:
    void run() override
    {
        for (int i = 0; i < 3; ++i) { // 每个线程尝试获取资源3次
            if (semaphore.tryAcquire(1)) { // 尝试获取一个资源(非阻塞)
                qDebug() << "Thread" << resourceId << "acquired a resource.";

                // 模拟资源使用
                msleep(2); // 睡眠2秒表示资源正在被使用

                // 释放资源
                semaphore.release(1);
                qDebug() << "Thread" << resourceId << "released a resource.";
            } else {
                qDebug() << "Thread" << resourceId << "could not acquire a resource, waiting...";
                // 如果无法获取资源,则等待直到有资源可用
                semaphore.acquire(1); // 这将阻塞线程直到资源可用
                qDebug() << "Thread" << resourceId << "acquired a resource after waiting.";

                // 模拟资源使用
                msleep(2); // 睡眠2秒表示资源正在被使用

                // 释放资源
                semaphore.release(1);
                qDebug() << "Thread" << resourceId << "released a resource after use.";
            }
        }
    }
};

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    // 创建并启动线程
    ResourceThread thread1(1);
    ResourceThread thread2(2);

    thread1.start();
    thread2.start();

    // 等待线程完成
    thread1.wait();
    thread2.wait();

    return a.exec();
}

注意

  1. 在这个示例中,QSemaphore 被初始化为5,表示有5个资源可用。
  2. tryAcquire(1) 尝试非阻塞地获取一个资源。如果成功,它返回 true,否则返回 false
  3. acquire(1) 尝试获取一个资源,如果当前没有可用资源,它将阻塞线程直到资源可用。
  4. release(1) 释放一个资源,增加信号量的值,可能允许其他等待的线程继续执行。
  5. 线程在尝试获取资源时可能会等待,这取决于资源的可用性和其他线程的行为。
  6. 在实际应用中,你应该根据资源的实际数量和线程的需求来设置信号量的初始值。
  7. 由于信号量不涉及互斥锁,因此多个线程可以同时获取资源(只要资源数量允许)。如果你需要确保只有一个线程能够访问共享资源,你应该使用 QMutex 而不是 QSemaphore

运行结果:

 4.6 使用QWaitCondition实现线程同步与互斥

   QWaitCondition 是 Qt 框架中用于线程间同步的类之一。它允许一个线程等待另一个线程发出信号,从而实现线程间的协调和同步。

        在使用 QWaitCondition 时,通常会配合使用 QMutex。QMutex 用于保护共享资源,而 QWaitCondition 则用于在等待某个条件为真时挂起线程,并在条件满足时唤醒线程。

#include <QCoreApplication>
#include <QThread>
#include <QMutex>
#include <QWaitCondition>
#include <QDebug>

QMutex mutex;
QWaitCondition condition;
bool ready = false;

class WorkerThread : public QThread {
public:
    void run() override {
        mutex.lock();
        while (!ready) {
            condition.wait(&mutex); // 等待条件满足
        }
        // 执行任务
        qDebug() << "Worker thread is running";
        mutex.unlock();
    }
};

int main(int argc, char *argv[]) {
    QCoreApplication a(argc, argv);

    WorkerThread worker;
    worker.start();

    // 模拟一些准备工作
    QThread::sleep(2);

    mutex.lock();
    ready = true; // 改变条件
    condition.wakeAll(); // 唤醒等待的线程
    mutex.unlock();

    worker.wait(); // 等待工作线程完成

    return a.exec();
}

在这个示例中,我们创建了一个WorkerThread类,它继承自QThread。在run方法中,线程首先获取互斥锁,并等待条件变量ready变为true。主线程在模拟一些准备工作后,改变条件变量ready的值,并唤醒等待的线程。工作线程被唤醒后,继续执行剩余的任务。

综上所述,Qt提供了多种线程同步与互斥机制,以确保多线程程序的正确运行。开发者可以根据具体需求选择合适的机制来实现线程间的同步与互斥。

五、Qt 线程使用选择

生命周期

开发任务

解决方案

一次调用

在另一个线程中运行一个函数,函数完成时退出线程

编写函数,使用QtConcurrent::run 运行它

派生QRunnable,使用QThreadPool::globalInstance()->start() 运行它

派生QThread,重新实现QThread::run() ,使用QThread::start() 运行它

一次调用

需要操作一个容器中所有的项。使用处理器所有可用的核心。一个常见的例子是从图像列表生成缩略图。

QtConcurrent 提供了map()函你数来将操作应用到容器中的每一个元素,提供了fitler()函数来选择容器元素,以及指定reduce函数作为选项来组合剩余元素。

一次调用

一个耗时运行的操作需要放入另一个线程。在处理过程中,状态信息需要发送会GUI线程。

使用QThread,重新实现run函数并根据需要发送信号。使用信号槽的queued连接方式将信号连接到GUI线程的槽函数。

持久运行

生存在另一个线程中的对象,根据要求需要执行不同的任务。这意味着工作线程需要双向的通讯。

派生一个QObject对象并实现需要的信号和槽,将对象移动到一个运行有事件循环的线程中并通过queued方式连接的信号槽进行通讯。

持久运行

生存在另一个线程中的对象,执行诸如轮询端口等重复的任务并与GUI线程通讯。

同上,但是在工作线程中使用一个定时器来轮询。尽管如此,处理轮询的最好的解决方案是彻底避免它。有时QSocketNotifer是一个替代。

参考:

  1. 【Qt】Qt的线程(两种QThread类的详细使用方式)_qt thread多线程 提示没有该名字-CSDN博客
  2. 一文搞定之Qt多线程(QThread、moveToThread)_qt 多线程-CSDN博客
  3. QThread使用——关于run和movetoThread的区别_movetothread和run区别-CSDN博客
  4. QT开发(三十四)——QT多线程编程_51CTO博客_qt多线程

猜你喜欢

转载自blog.csdn.net/a8039974/article/details/143268706