【Qt 线程】探索Qt线程编程的奥秘:多角度深入剖析(一)

简介: 【Qt 线程】探索Qt线程编程的奥秘:多角度深入剖析

一、Qt线程编程基础(Qt Threading Basics)

1.1 线程概念与基本概念(Thread Concepts and Fundamentals)

线程是操作系统调度执行的最小单元。它们在一个进程中运行,共享相同的内存空间。每个线程都有自己的独立执行路径和独立的栈,但它们可以访问相同的全局变量和其他资源。这使得线程之间的通信和数据共享变得相对容易,但也带来了同步和数据一致性方面的挑战。

在Qt中,主线程负责处理用户界面和事件循环,而子线程可以执行后台任务,如文件读写、网络请求和数据处理等。Qt提供了一套用于创建和管理线程的API,使得在Qt应用程序中使用多线程变得简单和直观。

以下是与线程编程相关的一些基本概念:

  • 并发(Concurrency):多个任务在同一时间段内交替执行,但不一定同时执行。例如,在单核处理器上运行的多线程应用程序。
  • 并行(Parallelism):多个任务同时执行。例如,在多核处理器上运行的多线程应用程序。
  • 互斥锁(Mutex):用于保护共享资源,防止多个线程同时访问。当一个线程获得互斥锁时,其他线程必须等待该线程释放锁。
  • 信号量(Semaphore):用于控制对共享资源的访问次数。信号量维护一个计数器,当计数器大于零时,线程可以访问资源;否则,线程必须等待。
  • 条件变量(Condition Variable):用于在线程之间同步特定条件的变化。当一个线程等待特定条件时,它会阻塞,直到另一个线程满足该条件并发出通知。

1.2 Qt线程类简介:QThread(Introduction to Qt Thread Class: QThread)

QThread是Qt线程编程中的核心类,它提供了创建和管理线程的功能。QThread类继承自QObject,这意味着它可以使用信号与槽机制进行线程间通信。在使用QThread时,通常有两种方法来实现线程编程:子类化QThread和使用worker对象。

QThread的主要成员函数

  • void start(QThread::Priority priority = QThread::InheritPriority):启动线程,并指定线程优先级。默认情况下,线程的优先级与创建它的线程相同。
  • void run():线程的入口函数。当使用子类化QThread方法时,需要重写此函数以实现线程的逻辑。
  • void quit():请求线程退出。这将导致事件循环结束,但不会等待线程真正结束。要等待线程结束,需要调用wait()函数。
  • void terminate():强制结束线程。此方法极不推荐使用,因为它可能导致资源泄漏和数据不一致。
  • bool wait(unsigned long time = ULONG_MAX):阻塞调用线程,直到QThread结束或指定的时间(毫秒)超时。返回值表示线程是否已经结束。

QThread的信号

  • void started():线程启动时发出的信号。
  • void finished():线程结束时发出的信号。

QThread的使用方法

  1. 子类化QThread:创建一个QThread的子类,并重写run()函数。这种方法适用于线程处理逻辑较为复杂的情况,或者需要更多控制权的情况。
  2. 使用worker对象:将线程处理逻辑封装到一个worker对象(通常是QObject的子类),并将其移动到QThread实例中。这种方法充分利用了Qt的信号与槽机制,更符合Qt的编程风格,同时在某些情况下也更容易实现。

1.3 Qt线程安全与同步机制(Qt Thread Safety and Synchronization Mechanisms)

在多线程编程中,线程安全和同步是至关重要的。当多个线程访问共享资源时,必须确保在同一时刻只有一个线程能够对资源进行修改,以防止数据不一致和竞争条件。Qt提供了一些同步机制,以帮助开发者实现线程安全。

互斥锁(QMutex)

QMutex是Qt提供的互斥锁实现,用于保护共享资源。使用QMutex时,需要在访问共享资源前锁定互斥锁,并在访问结束后解锁。QMutex有两种锁定模式:非递归和递归。在非递归模式下,同一个线程多次锁定互斥锁会导致死锁;而在递归模式下,同一个线程可以多次锁定互斥锁,但必须对应解锁相同次数。

读写锁(QReadWriteLock)

QReadWriteLock是Qt提供的读写锁实现。与QMutex相比,读写锁允许多个线程同时进行读取操作,而对写入操作进行排他控制。这可以提高多线程应用程序的性能,特别是在读操作较多的场景中。使用QReadWriteLock时,需要区分锁定读锁和写锁,并在访问结束后分别解锁。

信号量(QSemaphore)

QSemaphore是Qt提供的信号量实现。信号量用于限制对共享资源的访问次数,以实现并发控制。信号量维护一个计数器,当计数器大于0时,线程可以访问资源。当一个线程成功访问资源后,信号量计数器减1;当线程释放资源后,信号量计数器加1。如果计数器为0,线程必须等待其他线程释放资源。

条件变量(QWaitCondition)

QWaitCondition是Qt提供的条件变量实现。条件变量用于在线程间同步特定条件的变化。当一个线程等待特定条件时,它会阻塞并释放锁,直到另一个线程满足条件并唤醒等待线程。QWaitCondition通常与QMutex或QReadWriteLock配合使用。

原子操作(QAtomic *)

Qt提供了一组原子操作类(如QAtomicInt、QAtomicPointer等),用于实现对整数和指针的原子操作。原子操作是线程安全的,不需要额外的同步机制。原子操作适用于对单个数据进行简单的修改操作,如计数器、标志等。

二、Qt线程编程方法一:子类化QThread(Subclassing QThread)

2.1 创建自定义线程类(Creating Custom Thread Class)

子类化QThread是实现Qt线程编程的一种方法。在这种方法中,我们需要创建一个QThread的子类,并重写run()函数。run()函数是线程的入口点,线程处理逻辑应该在这个函数中实现。以下是创建自定义线程类的步骤:

  1. 创建自定义线程类
    首先,创建一个QThread的子类。例如,创建一个名为MyThread的自定义线程类:
#include <QThread>
class MyThread : public QThread
{
    Q_OBJECT
public:
    explicit MyThread(QObject *parent = nullptr);
protected:
    void run() override; // 重写run()函数
};
  1. 重写**run()**函数
    MyThread类中,重写run()函数以实现线程的处理逻辑。例如:
void MyThread::run()
{
    // 线程处理逻辑,例如执行耗时操作、文件读写等
    for (int i = 0; i < 100; ++i) {
        // 执行操作...
    }
}
  1. 实例化自定义线程类并启动线程
    在应用程序中,可以通过实例化自定义线程类并调用start()函数来启动线程。例如:
MyThread *myThread = new MyThread();
myThread->start(); // 启动线程

在子类化QThread的方法中,线程处理逻辑与QThread类紧密耦合。这种方法适用于线程处理逻辑较为复杂的情况,或者需要更多控制权的情况。然而,这种方法可能不太符合Qt的信号与槽编程风格。在下一节中,我们将介绍另一种实现Qt线程编程的方法:使用worker对象。

2.2 实现线程处理函数(Implementing Thread Processing Function)

在子类化QThread的方法中,我们需要在自定义线程类中重写run()函数来实现线程的处理逻辑。下面我们将详细讨论如何实现线程处理函数,以及如何在处理函数中处理事件和信号。

实现线程处理逻辑

在重写的run()函数中,可以执行需要在新线程中运行的任务,例如耗时操作、文件读写、数据处理等。请注意,run()函数中的代码应该遵循线程安全规则,尤其是在访问共享资源时。可以使用Qt提供的同步机制,如QMutex、QReadWriteLock等,来确保线程安全。

以下是一个简单的示例,展示了在run()函数中实现线程处理逻辑:

void MyThread::run()
{
    for (int i = 0; i < 100; ++i) {
        // 执行耗时操作,例如计算或文件读写
        QThread::msleep(10); // 模拟耗时操作,暂停10毫秒
    }
}

处理事件和信号

在自定义线程类中,可以使用信号与槽机制进行线程间通信。但是,请注意不要在run()函数中执行事件循环(exec()),因为这可能导致线程阻塞。相反,可以在run()函数中发射信号,以通知其他线程处理结果或状态变化。

例如,可以在MyThread类中定义一个名为progressChanged的信号:

class MyThread : public QThread
{
    Q_OBJECT
public:
    explicit MyThread(QObject *parent = nullptr);
protected:
    void run() override;
signals:
    void progressChanged(int progress); // 定义一个信号
};

然后,在run()函数中适当的地方发射progressChanged信号:

void MyThread::run()
{
    for (int i = 0; i < 100; ++i) {
        // 执行耗时操作,例如计算或文件读写
        QThread::msleep(10); // 模拟耗时操作,暂停10毫秒
        // 发射信号,通知其他线程处理进度已更改
        emit progressChanged(i);
    }
}

在应用程序中,可以将progressChanged信号连接到槽函数,以更新界面或执行其他操作。例如:

MyThread *myThread = new MyThread();
connect(myThread, &MyThread::progressChanged, this, &MainWindow::updateProgress);
myThread->start(); // 启动线程

在这个示例中,假设MainWindow类中有一个名为updateProgress的槽函数,用于更新进度条或其他界面元素。

请注意,Qt的信号与槽机制已经处理了线程安全问题,因此在槽函数中访问共享资源通常是安全的.

2.3 线程的终止与结束处理(Terminating Threads and Handling Thread Completion)

在子类化QThread的方法中,需要注意线程的终止和结束处理。以下是如何正确终止线程以及处理线程完成的一些建议。

终止线程

在某些情况下,可能需要提前终止线程。不推荐使用terminate()函数强制终止线程,因为这可能导致资源泄漏和数据不一致。相反,应该使用某种机制通知线程自然退出。

一种常用的方法是使用一个原子标志或条件变量,通知线程在完成当前任务后立即退出。例如,在自定义线程类中定义一个QAtomicInt变量作为停止标志:

class MyThread : public QThread
{
    Q_OBJECT
public:
    explicit MyThread(QObject *parent = nullptr);
    void stop(); // 添加一个停止函数
protected:
    void run() override;
private:
    QAtomicInt m_stopFlag; // 停止标志
};

stop()函数中设置停止标志,并在run()函数中检查停止标志:

void MyThread::stop()
{
    m_stopFlag.store(1); // 设置停止标志
}
void MyThread::run()
{
    for (int i = 0; i < 100; ++i) {
        // 检查停止标志,如果设置,则退出循环
        if (m_stopFlag.load()) {
            break;
        }
        // 执行耗时操作,例如计算或文件读写
        QThread::msleep(10); // 模拟耗时操作,暂停10毫秒
    }
}

处理线程完成

当线程完成任务并自然退出时,可以使用finished()信号来通知其他线程。例如,在应用程序中,可以将finished()信号连接到槽函数,以进行清理操作或通知用户:

MyThread *myThread = new MyThread();
connect(myThread, &MyThread::finished, this, &MainWindow::handleThreadFinished);
myThread->start(); // 启动线程

在这个示例中,假设MainWindow类中有一个名为handleThreadFinished的槽函数,用于处理线程完成事件。

另外,也可以使用wait()函数等待线程结束。wait()函数阻塞调用线程,直到线程完成或超时。例如,在应用程序中,可以在线程结束前等待线程完成任务:

MyThread *myThread = new MyThread();
myThread->start(); // 启动线程
myThread->wait(); // 等待线程结束

请注意,wait()函数可能导致界面冻结,因此不推荐在主线程中使用。如果需要在主线程中等待线程结束,请考虑使用信号与槽机制。

三、Qt线程编程方法二:使用worker对象和moveToThread(Using Worker Objects and moveToThread)

3.1 创建worker对象(Creating Worker Objects)

在这种方法中,需要创建一个继承自QObject的worker类,并在其中实现线程处理逻辑。worker类应该包含线程处理函数和信号槽。例如,创建一个名为MyWorker的worker类:

#include <QObject>
class MyWorker : public QObject
{
    Q_OBJECT
public:
    explicit MyWorker(QObject *parent = nullptr);
public slots:
    void process(); // 线程处理函数
signals:
    void progressChanged(int progress); // 用于通知其他线程的信号
};

然后,实现process()函数以实现线程处理逻辑:

void MyWorker::process()
{
    for (int i = 0; i < 100; ++i) {
        // 执行耗时操作,例如计算或文件读写
        QThread::msleep(10); // 模拟耗时操作,暂停10毫秒
        // 发射信号,通知其他线程处理进度已更改
        emit progressChanged(i);
    }
}

3.2 使用moveToThread将worker对象移入新线程(Moving Worker Objects to New Thread using moveToThread)

创建worker对象后,可以使用moveToThread()函数将其移动到一个新线程。以下是如何将worker对象移动到新线程的步骤:

  1. 实例化QThread和worker对象
    在应用程序中,实例化一个QThread对象和一个worker对象:
QThread *thread = new QThread();
MyWorker *worker = new MyWorker();
  1. 将worker对象移动到新线程
    使用moveToThread()函数将worker对象移动到新线程:
worker->moveToThread(thread);
  1. 连接信号与槽
    使用信号与槽连接线程和worker对象。例如,当线程启动时,调用worker对象的process()函数:
connect(thread, &QThread::started, worker, &MyWorker::process);
  1. 也可以连接其他信号,如finished()信号和progressChanged信号。
  2. 启动线程
    调用QThread对象的start()函数启动线程:
thread->start();

使用worker对象和moveToThread()函数的方法允许将线程处理逻辑与QThread类解耦,使代码更易于维护和扩展。此外,这种方法更符合Qt的信号与槽编程风格。

3.3 worker对象的终止与结束处理(Terminating Worker Objects and Handling Completion)

在使用worker对象和moveToThread()方法时,我们同样需要注意线程的终止和结束处理。以下是如何正确终止线程以及处理线程完成的一些建议。

终止线程

与子类化QThread的方法类似,我们不推荐使用terminate()函数强制终止线程。相反,应该使用某种机制通知worker对象自然退出。

在worker类中,可以定义一个槽函数来设置停止标志。例如,在MyWorker类中定义一个stop()槽函数:

class MyWorker : public QObject
{
    Q_OBJECT
public:
    explicit MyWorker(QObject *parent = nullptr);
public slots:
    void process(); // 线程处理函数
    void stop(); // 添加一个停止函数
signals:
    void progressChanged(int progress); // 用于通知其他线程的信号
private:
    QAtomicInt m_stopFlag; // 停止标志
};

然后,在stop()函数中设置停止标志,并在process()函数中检查停止标志:

void MyWorker::stop()
{
    m_stopFlag.store(1); // 设置停止标志
}
void MyWorker::process()
{
    for (int i = 0; i < 100; ++i) {
        // 检查停止标志,如果设置,则退出循环
        if (m_stopFlag.load()) {
            break;
        }
        // 执行耗时操作,例如计算或文件读写
        QThread::msleep(10); // 模拟耗时操作,暂停10毫秒
        // 发射信号,通知其他线程处理进度已更改
        emit progressChanged(i);
    }
}

在应用程序中,可以使用信号与槽连接来请求worker对象停止:

connect(this, &MainWindow::stopRequested, worker, &MyWorker::stop);

在这个示例中,假设MainWindow类中有一个名为stopRequested的信号,用于通知worker对象停止处理。

处理线程完成

当worker对象完成任务并自然退出时,可以使用信号与槽机制来通知其他线程。例如,可以在MyWorker类中定义一个名为finished的信号:

class MyWorker : public QObject
{
    Q_OBJECT
public:
    explicit MyWorker(QObject *parent = nullptr);
public slots:
    void process(); // 线程处理函数
    void stop(); // 停止函数
signals:
    void progressChanged(int progress); // 用于通知其他线程的信号
    void finished(); // 处理完成信号
};

然后,在process()函数中适当的地方发射finished信号:

void MyWorker::process()
{
    for (int i = 0; i < 100; ++i) {
        // 检查停止标志,如果设置,则退出循环
        if (m_stopFlag.load()) {
            break;
        }
        // 执行耗时操作,例如计算或文件读写
  QThread::msleep(10); // 模拟耗时操作,暂停10毫秒
  // 发射信号,通知其他线程处理进度已更改
  emit progressChanged(i);
  }
  // 在循环结束后发射完成信号
  emit finished();
}

在应用程序中,可以将`finished`信号连接到槽函数,以进行清理操作或通知用户:

connect(worker, &MyWorker::finished, this, &MainWindow::handleWorkerFinished);

在这个示例中,假设MainWindow类中有一个名为handleWorkerFinished的槽函数,用于处理线程完成事件。

3.4 清理线程资源(Cleaning Up Thread Resources)

当worker对象和线程不再需要时,应该正确地清理资源。可以在worker对象的finished信号和线程的finished信号中处理资源清理。

例如,在应用程序中,可以将worker对象的finished信号连接到线程的quit()槽,以便在worker对象完成任务时停止线程:

connect(worker, &MyWorker::finished, thread, &QThread::quit);

然后,可以将线程的finished信号连接到worker对象和线程的deleteLater槽,以便在线程停止后删除对象:

connect(thread, &QThread::finished, worker, &MyWorker::deleteLater);
connect(thread, &QThread::finished, thread, &QThread::deleteLater);

使用这种连接方式,可以确保在worker对象和线程不再需要时,资源得到正确的清理。

总之,使用worker对象和moveToThread()方法是一种更符合Qt信号与槽编程风格的线程处理方法。通过将线程处理逻辑与QThread类解耦,这种方法可以使代码更易于维护和扩展。


【Qt 线程】探索Qt线程编程的奥秘:多角度深入剖析(二)https://developer.aliyun.com/article/1464090

目录
相关文章
|
4天前
|
Java 调度 数据库
Java并发编程:深入理解线程池
在Java并发编程的海洋中,线程池是一艘强大的船,它不仅提高了性能,还简化了代码结构。本文将带你潜入线程池的深海,探索其核心组件、工作原理及如何高效利用线程池来优化你的并发应用。
|
4天前
|
监控 Java 测试技术
Java并发编程最佳实践:设计高性能的多线程系统
Java并发编程最佳实践:设计高性能的多线程系统
18 1
|
4天前
|
设计模式 安全 Java
Java并发编程实战:使用synchronized关键字实现线程安全
Java并发编程实战:使用synchronized关键字实现线程安全
16 0
|
8天前
|
Java 调度 开发者
Java 神秘新成员 —— 虚拟线程究竟是什么?它又能解开哪些编程痛点之谜?
【8月更文挑战第23天】Java虚拟线程是一种轻量级执行线程,由Java运行时管理,相较于传统操作系统线程,其创建和管理成本更低。基于用户模式线程概念,Java应用程序无需依赖OS即可实现高度并发。虚拟线程数量可远超传统线程,有效提升多核处理器利用率和并发性能。它解决了传统Java线程模型中创建成本高、调度开销大及I/O阻塞等问题,极大提高了程序的并发性和响应速度。
15 1
|
2天前
|
机器学习/深度学习 Java TensorFlow
深度学习中的图像识别:从理论到实践Java中的多线程编程入门指南
【8月更文挑战第29天】本文将深入探讨深度学习在图像识别领域的应用,从基础理论到实际应用案例,带领读者一步步理解如何利用深度学习技术进行图像识别。我们将通过一个简单的代码示例,展示如何使用Python和TensorFlow库实现一个基本的图像识别模型。无论你是初学者还是有一定经验的开发者,都能从中获得启发和学习。 【8月更文挑战第29天】在Java世界里,线程是程序执行的最小单元,而多线程则是提高程序效率和响应性的关键武器。本文将深入浅出地引导你理解Java多线程的核心概念、创建方法以及同步机制,帮助你解锁并发编程的大门。
|
4天前
|
安全 Java API
Java多线程编程:使用Atomic类实现原子操作
在Java多线程环境中,共享资源的并发访问可能导致数据不一致。传统的同步机制如`synchronized`关键字或显式锁虽能保障数据一致性,但在高并发场景下可能导致线程阻塞和性能下降。为此,Java提供了`java.util.concurrent.atomic`包下的原子类,利用底层硬件的原子操作确保变量更新的原子性,实现无锁线程安全。
8 0
|
4天前
|
安全 Java
深入Java并发编程:线程同步与互斥机制
在多线程编程中,确保数据一致性与避免竞态条件至关重要。Java提供了多种同步机制,如`synchronized`关键字、显式锁(如`ReentrantLock`)和原子变量(如`AtomicInteger`)。这些工具帮助解决并发问题,例如竞态条件(依赖线程执行顺序的问题)、死锁(线程互相等待对方持有的资源)和活锁(线程反复响应对方行为而无法进展)。合理运用这些机制可有效管理并发,确保程序稳定运行。
29 0
|
6天前
|
Java 开发者
解锁Java并发编程的秘密武器!揭秘AQS,让你的代码从此告别‘锁’事烦恼,多线程同步不再是梦!
【8月更文挑战第25天】AbstractQueuedSynchronizer(AQS)是Java并发包中的核心组件,作为多种同步工具类(如ReentrantLock和CountDownLatch等)的基础。AQS通过维护一个表示同步状态的`state`变量和一个FIFO线程等待队列,提供了一种高效灵活的同步机制。它支持独占式和共享式两种资源访问模式。内部使用CLH锁队列管理等待线程,当线程尝试获取已持有的锁时,会被放入队列并阻塞,直至锁被释放。AQS的巧妙设计极大地丰富了Java并发编程的能力。
16 0
|
8天前
|
存储 安全 Unix
并发编程基础:使用POSIX线程(pthread)进行多线程编程。
并发编程基础:使用POSIX线程(pthread)进行多线程编程。
36 0
|
9天前
|
安全 Java 测试技术
Java 中多线程编程的最佳实践
【8月更文挑战第22天】
19 0