[QT编程系列-31]:多线程机制 - Qthread工作原理

目录

一、Qthread工作原理

二、QThread的start()方法

三、QT 线程的生命周期

四、事件循环机制

五、QThread自带信号

六、如何在run函数中多次执行

七、QThread什么时候发送finished信号

八、为什么不建议改写run函数


一、Qthread工作原理

QThread是Qt中用于管理线程的类,它提供了一些方法和事件循环机制来控制线程的生命周期和行为。

下面是QThread的内部工作原理的简要描述:

  1. 线程创建和启动:当使用QThread创建一个新的线程时,系统会为该线程分配资源并启动它。在调用QThread的start()方法后,会触发内部的线程创建和启动过程。

  2. run()函数执行:QThread类内部有一个虚拟函数run(),该函数定义了线程的执行逻辑启动线程后,它会自动调用run()函数,在该函数中执行线程需要完成的任务。有些情况下,我们需要创建自定义的线程类,并重写run()函数,在其中实现线程的具体逻辑。

  3. 事件循环管理:QThread为线程提供了一个事件循环机制,用于处理事件和信号。在run()函数中,默认会启动线程的事件循环事件循环会接收并分发来自其他对象的事件和信号。在事件循环中,可以处理特定的任务、响应特定事件和信号。

  4. 退出线程:当线程完成任务或需要终止时,可以调用QThread的quit()方法来退出线程的事件循环。此外,退出线程还可以使用terminate()方法,但需要注意它可能会导致资源泄漏和不安全的情况,因此建议使用quit()进行线程退出。

  5. 销毁线程:线程在完成任务后,可以调用QThread的wait()方法,使线程等待直到其执行完成。对于需要重复使用的线程,可以选择不销毁线程,而是使用wait()方法来等待线程重新启动。

总体上,QThread通过封装底层的线程管理和事件循环机制,提供了一种方便的方式来管理线程的创建、启动和停止,并提供了线程间通信的能力。为了正确使用QThread,应了解线程的生命周期、事件循环机制以及线程安全性的考虑。合理地使用QThread可以帮助你编写出可靠和高效的多线程应用程序。

二、QThread的start()方法

QThread的start()方法用于启动线程的执行。以下是关于QThread的start()方法的一些重要说明:

  1. 启动线程:通过调用start()方法,可以启动QThread对象所代表的线程。在调用start()方法后,QThread会执行线程的创建和启动过程。

  2. 自动调用run()函数:一旦线程启动,QThread会自动调用线程类中的run()函数。默认情况下,QThread的run()函数是空的,因此建议继承QThread并重写run()函数,在其中实现线程需要完成的任务。也就是说,run函数并不是QThread线程的主函数,而是QThread连接应用程序的接口,因此,run还是不能是死循环,否则,QThread线程的主函数就会在调用run函数后备阻塞!!!

  3. 线程创建和资源分配:在调用start()方法后,QThread会为线程分配资源,并创建线程。线程的创建和资源分配是由操作系统完成的,这意味着QThread的start()方法返回后,并不意味着线程立即开始执行。

  4. 线程状态变化:启动线程后,QThread的状态会发生变化。可以通过调用QThread的isRunning()方法来检查线程是否正在执行。主线程可以通过调用waitForStarted()方法来阻塞,直到线程开始执行。

  5. 重复启动:一般情况下,QThread对象可以多次使用start()方法来启动线程。但是,根据Qt文档的建议,为了可读性和清晰性,不建议多次启动一个已经运行过的线程。可以使用QThread的wait()方法来等待线程结束,然后再重复执行start()方法。

需要注意的是,QThread的start()方法只是启动线程,而不阻塞线程执行。如果主线程想要等待线程执行完成,可以使用QThread的wait()方法来阻塞当前线程,直到线程执行完成。此外,如果需要线程执行完成后才进行下一步操作,可以使用QThread的finished()信号来进行处理。

总结而言,QThread的start()方法是启动线程执行的方法,通过调用该方法,QThread对象所代表的线程会开始执行,并执行线程类中定义的run()函数。

三、QT 线程的生命周期

在Qt中,线程的生命周期通常包括以下几个阶段:

  1. 创建线程:创建线程的第一步是实例化一个QThread对象或其子类,并为该线程分配资源。通常,在主线程中创建线程对象。

  2. 启动线程:通过调用线程对象的start()方法来启动线程。QThread类会负责创建底层操作系统的线程,并开始执行线程的run()函数。如果线程不接受外部事件,可以用While(1) 循环,重写run函数;如果线程需要接收外部事件,则不建议用While(1)重写run函数,重新run函数的后果是,需要在run函数中实现信号队列机制!!!

  3. 运行线程:在线程启动后,运行线程会调用run()函数,该函数会在子线程中执行。在自定义的子类中,可以重写run()函数,以实现线程的主要逻辑。

  4. 执行任务:在线程的run()函数中,可以执行需要在线程中完成的任务,例如计算、访问网络或执行其他耗时操作。说明:执行耗时的事,意味着执行耗时任务后,需要停止执行run函数,不建议while(1)执行。

  5. 完成线程:在线程的任务完成后,可以调用QThread的exit()或quit()方法来终止线程的执行。此时,线程会退出运行,并依次触发线程的finished()信号。

  6. 释放资源:当线程完成后,Qt会自动释放线程的系统资源。线程对象也会被销毁,释放相应的内存。

需要注意的是,当线程退出后,它的状态逐渐变为终止状态。在终止状态下,线程不能再被重用。如果要再次使用线程,需要重新创建一个新的QThread对象。

此外,还可以通过调用QThread的wait()方法,在当前线程中等待子线程执行完成。这样可以确保在主线程中等待并处理子线程的结果,或是等待子线程完全退出后再进行下一步操作。

总结而言,Qt线程的生命周期包括线程的创建、启动、运行、完成和资源释放阶段。合理地管理线程的生命周期可以确保多线程应用程序的稳定性和可靠性。

四、事件循环机制

Qt中的事件循环(Event Loop)是一种机制,用于处理事件和信号并驱动应用程序的整个事件处理过程

事件循环主要由Qt的事件系统对象的信号与槽机制组成。

以下是关于事件循环机制的一些说明:

  1. 事件处理流程:在事件循环机制下,Qt应用程序会进入一个无限循环,该循环会不断处理各种事件,直到应用程序退出。在事件循环中,Qt会不停地检查事件是否发生,然后分发相应的事件到合适的对象进行处理。

  2. 事件源:事件源是导致事件发生的对象,可以是用户操作、系统事件或其他应用程序内部事件。例如,按钮的点击、定时器的超时、网络数据的到达等都可以作为事件源。

  3. 事件类型和事件过滤器:Qt事件系统提供了一系列事件类型,用于标识不同类型的事件,例如鼠标点击事件、键盘事件、定时器事件等。事件过滤器可以被用来截获和修改事件的处理过程。通过重写事件过滤器函数,可以在对象处理事件之前先拦截和处理事件。

  4. 事件分发和派发:当事件发生时,Qt会将事件分发到合适的对象进行处理。事件的分发是根据事件分发规则和事件传递机制来进行的。经过事件分发后,事件最终会被派发到目标对象的事件处理函数(例如QWidget的事件处理函数)进行具体的处理。

  5. 信号与槽机制:信号与槽机制是一种Qt特有的机制,用于对象之间的通信和事件处理。当对象发生某个特定的事件或状态发生变化时,可以通过发射信号来通知其他对象。其他对象可以将自己的槽函数与信号进行连接,以响应信号的发射并执行相应的处理逻辑。

  6. 线程和事件循环:每个线程都有一个独立的事件循环,这是关键!!!在多线程应用程序中,每个线程通常都有自己的事件循环,用于处理来自线程内部或外部的事件和信号。线程之间可以通过信号与槽机制进行异步的事件通信

        Qt的事件循环机制为开发者提供了一种响应用户操作和处理内部事件的方式。通过合理使用事件循环,可以使应用程序具备高响应性和良好的用户交互体验。同时,事件循环也是Qt跨平台能力的基础之一,在不同操作系统上提供一致的事件处理机制。

五、QThread自带信号

QThread类本身定义了一些信号,用于通知线程的状态变化或事件的发生。下面是一些常用的QThread信号:

  1. started:当线程开始执行时,QThread会发射started信号。可以通过连接该信号的槽函数来执行一些初始化操作。

  2. finished:当线程执行完成并即将退出时,QThread会发射finished信号。可以通过连接该信号的槽函数来进行一些清理操作,如释放资源。

  3. terminated:当线程被终止时,QThread会发射terminated信号。这个信号一般在调用QThread的terminate()方法后发出。然而,建议避免使用terminate()方法,因为它可能导致资源泄漏和不安全的情况。

除了上述由QThread类本身发射的信号外,你也可以在自定义的线程类中定义自己的信号,以进行线程间的通信。在自定义线程类中,使用Qt中的信号与槽机制,可以定义和发射自己的信号,供其他对象连接和处理。

以下是一个示例,演示了如何在自定义的线程类中定义和发射自定义信号:

#include 
#include 

class MyThread : public QThread
{
    Q_OBJECT

signals:
    void progressChanged(int value);

protected:
    void run() override {
        for (int i = 0; i <= 100; ++i) {
            // 执行任务
            // ...

            // 发射自定义信号
            emit progressChanged(i);

            // 线程睡眠一段时间
            msleep(100);
        }
    }
};

在这个示例中,自定义的线程类MyThread通过信号progressChanged(int value)来通知任务的进度变化。在run()函数中,每次任务执行完毕后,通过emit关键字发射progressChanged信号,传递当前任务进度的值。其他对象可以连接这个信号,并在槽函数中处理进度变化。

总结而言,QThread提供了一些预定义的信号来通知线程的状态变化,同时也可以在自定义的线程类中定义和发射自己的信号。通过信号与槽机制,可以在线程之间进行异步的事件通信。

六、如何在run函数中多次执行

在Qt中,为了多次执行QThread的run函数,你可以通过重写QThread子类的run函数并在循环中调用它。以下是一个示例代码,演示了如何多次执行QThread的run函数:

#include 
#include 

class MyThread : public QThread
{
protected:
    void run() override {
        for (int i = 0; i < 5; ++i) {
            // 执行任务
            qDebug() << "Executing run function, iteration:" << i;

            // 线程睡眠一段时间
            sleep(1);
        }
    }
};

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

    MyThread thread;
    thread.start(); // 启动线程

    if (thread.isRunning()) {
        thread.wait(); // 等待线程执行完成
        thread.quit(); // 结束线程的事件循环,或者可以调用thread.exit()
    }

    return a.exec();
}

在这个示例中,我们定义了一个名为MyThread的子类,重写了run函数。在循环中,我们执行一些任务,并打印当前的迭代次数。线程在每次任务执行完成后,睡眠1秒钟,然后继续下一次迭代,直到达到5次。

在主函数中,我们实例化了一个MyThread对象并启动线程。然后,我们使用thread.wait()来等待线程执行完成,并通过thread.quit()来结束线程的事件循环(也可以使用thread.exit())。最后,我们调用QCoreApplication的exec()函数来启动整个应用程序的事件循环。

需要注意的是,这个示例是在主线程中创建和操作QThread对象的,也就是在Qt的主事件循环中执行的。如果需要在其他线程中执行QThread的run函数,你需要相应地调整代码。

总结而言,如果想要多次执行QThread的run函数,你可以通过重写run函数并在循环中调用它来实现。启动线程后,可以使用wait函数等待线程完成,并通过quit或exit函数来结束线程的事件循环。

七、QThread什么时候发送finished信号

在Qt中,QThread对象会在以下情况下发送finished信号:

  1. 当线程的run()函数执行完成后,线程会发送finished信号。这表示线程的任务已经完成,线程即将退出。你可以在QThread的子类或者直接使用QThread对象时连接finished信号来执行相关的操作。

  2. 如果调用了QThread对象的exit()或quit()函数,线程同样会发送finished信号。这两个函数会导致线程退出,并触发finished信号。exit()函数用于终止线程,而quit()函数则会结束线程的事件循环并退出线程。

需要注意的是,finished信号是在线程的上下文中发射的。这意味着,如果你想在主线程(或其他线程)中处理finished信号,你需要使用Qt的信号与槽机制,确保连接的槽函数所在的对象位于正确的线程上。

以下是一个示例代码,演示了在QThread的子类中发送finished信号:

#include 
#include 

class MyThread : public QThread
{
public:
    void run() override {
        qDebug() << "Thread started";

        // 模拟耗时任务
        for (int i = 0; i < 5; ++i) {
            qDebug() << "Working on task" << i;
            msleep(1000);
        }

        qDebug() << "Thread finished";
        emit finished(); // 发送finished信号
    }
};

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

    MyThread thread;
    QObject::connect(&thread, &MyThread::finished, [&a]() {
        qDebug() << "Thread finished signal received";
        a.quit(); // 结束应用程序的事件循环
    });

    thread.start(); // 启动线程

    return a.exec();
}

在这个示例中,我们定义了一个MyThread的子类,并重写了run()函数。在run()函数中,我们执行了一些模拟的耗时任务,每个任务耗时1秒。任务完成后,发送了finished信号表示线程即将退出。

在主函数中,我们创建了一个MyThread对象,并连接了finished信号到一个lambda槽函数。在槽函数中,我们简单地打印出"Thread finished signal received",然后调用QCoreApplication的quit()函数来结束应用程序的事件循环。

总结而言,QThread对象会在线程的run函数执行完成后或调用exit/quit函数时发送finished信号。你可以通过连接该信号来执行相关的操作,如清理资源或结束应用程序的事件循环。

八、为什么不建议改写run函数

Qt不建议改写QThread的run函数,这是因为在Qt中,QThread是一个底层线程管理类,用于创建和管理线程。

默认情况下,QThread的run函数是一个空函数,不执行任何操作。

有以下几个原因说明为什么不建议改写run函数:

  1. QThread的run函数是在新创建的线程中执行。这意味着,如果你在QThread的run函数中执行耗时操作,会阻塞新创建线程的线程,影响新创建线程的响应性。

  2. run函数的执行完全取决于调用start函数。当调用QThread对象的start函数时,它会启动线程并在新线程中执行内部的run函数。但是,如果你重写了run函数,在外部手动调用run函数时,它仍会在当前线程中执行,而不是在新线程中执行。这样做会导致和预期的不一致。

  3. 无法正确处理线程的事件循环。QThread内部实现了一个事件循环,用于处理线程中的事件和信号。如果你重写了run函数并在其中添加了自己的事件循环,可能会导致线程管理和事件分发机制失效,引发一些难以预测的问题这是不自定义run函数的最重要的原因!!

为了解决上述问题,通常建议的做法是派生一个新的类,并重写其自己的函数来执行实际的工作。该派生类可以通过信号和槽机制与其他对象进行通信,可以在其执行完成后自动删除。然后,将这个派生类的实例作为QThread对象的成员变量,并在QThread对象的run函数中调用派生类的函数。

以下是一个示例,演示了使用派生类的方式来执行实际工作:

#include 
#include 

class Worker : public QObject
{
    Q_OBJECT

public slots:
    void doWork() {
        // 执行任务
        qDebug() << "Worker: Doing work";
        // ...
    }
};

class MyThread : public QThread
{
public:
    MyThread() {
        //在线程的构造函数中,完成线程内部对象的创建和上下文的迁移!!!
        //完成代码的隔离
        //在线程的上下文空间中完成对象的线程上下文的迁移
        worker = new Worker;
        worker->moveToThread(this); //这是关键!!!

        // 连接线程的started信号到任务执行的槽函数
        connect(this, &QThread::started, worker, &Worker::doWork);
    }

    ~MyThread() {
        delete worker;
    }

private:
    Worker* worker;

protected:
    void run() override {
        // 不执行任何操作,由Worker类的doWork槽函数处理任务
    }
};

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

    MyThread thread;
    thread.start(); // 启动线程

    // Do something else in the main thread

    return a.exec();
}

在这个示例中,我们创建了一个Worker类来执行实际的工作,这个类继承自QObject。MyThread类继承自QThread,并在构造函数中创建了Worker的实例,并将其移动到线程上。然后,我们将线程的started信号连接到Worker的doWork槽函数上,以触发任务的执行。MyThread的run函数保持为空,不执行任何操作。

总结而言,Qt不建议改写QThread的run函数,因为run函数不会在新创建的线程中执行,并且会影响线程的事件循环机制。相反,建议派生一个新的类来执行实际工作,并通过信号与槽机制在QThread对象的run函数中触发执行。这样可以保持线程管理和事件分发机制的一致性和可靠性。

你可能感兴趣的:(编程系列-QT,qt,开发语言,C++)