【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

目录
相关文章
|
6天前
|
安全 前端开发 程序员
|
2天前
|
Java 程序员 调度
Java中的多线程编程:基础知识与实践
【5月更文挑战第19天】多线程编程是Java中的一个重要概念,它允许程序员在同一时间执行多个任务。本文将介绍Java多线程的基础知识,包括线程的创建、启动和管理,以及如何通过多线程提高程序的性能和响应性。
|
3天前
|
Java
深入理解Java并发编程:线程池的应用与优化
【5月更文挑战第18天】本文将深入探讨Java并发编程中的重要概念——线程池。我们将了解线程池的基本概念,应用场景,以及如何优化线程池的性能。通过实例分析,我们将看到线程池如何提高系统性能,减少资源消耗,并提高系统的响应速度。
13 5
|
3天前
|
消息中间件 安全 Java
理解Java中的多线程编程
【5月更文挑战第18天】本文介绍了Java中的多线程编程,包括线程和多线程的基本概念。Java通过继承Thread类或实现Runnable接口来创建线程,此外还支持使用线程池(如ExecutorService和Executors)进行更高效的管理。多线程编程需要注意线程安全、性能优化和线程间通信,以避免数据竞争、死锁等问题,并确保程序高效运行。
|
3天前
|
安全 Java 容器
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第18天】随着多核处理器的普及,并发编程变得越来越重要。Java提供了丰富的并发编程工具,如synchronized关键字、显式锁Lock、原子类、并发容器等。本文将深入探讨Java并发编程的核心概念,包括线程安全、死锁、资源竞争等,并分享一些性能优化的技巧。
|
3天前
|
安全 Java 开发者
Java中的多线程编程:理解与实践
【5月更文挑战第18天】在现代软件开发中,多线程编程是提高程序性能和响应速度的重要手段。Java作为一种广泛使用的编程语言,其内置的多线程支持使得开发者能够轻松地实现并行处理。本文将深入探讨Java多线程的基本概念、实现方式以及常见的并发问题,并通过实例代码演示如何高效地使用多线程技术。通过阅读本文,读者将对Java多线程编程有一个全面的认识,并能够在实际开发中灵活运用。
|
6天前
|
安全 Java 开发者
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第15天】本文将深入探讨Java并发编程的核心概念,包括线程安全和性能优化。我们将通过实例分析,理解线程安全的重要性,并学习如何通过各种技术和策略来实现它。同时,我们也将探讨如何在保证线程安全的同时,提高程序的性能。
|
6天前
|
消息中间件 并行计算 Java
Java中的多线程编程:基础知识与实践
【5月更文挑战第15天】 在现代计算机编程中,多线程是一个复杂但必不可少的概念。特别是在Java这种广泛使用的编程语言中,理解并掌握多线程编程是每个开发者必备的技能。本文将深入探讨Java中的多线程编程,从基础概念到实际应用场景,为读者提供全面的理论支持和实践指导。
|
6天前
|
Java 程序员 调度
Java中的多线程编程:从理论到实践
【5月更文挑战第14天】在现代计算机技术中,多线程编程是一个重要的概念。它允许多个线程并行执行,从而提高程序的运行效率。本文将从理论和实践两个角度深入探讨Java中的多线程编程,包括线程的基本概念、创建和控制线程的方法,以及如何处理线程同步和通信问题。
|
6天前
|
Java Linux 调度