一、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的使用方法
- 子类化QThread:创建一个QThread的子类,并重写
run()
函数。这种方法适用于线程处理逻辑较为复杂的情况,或者需要更多控制权的情况。 - 使用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()
函数是线程的入口点,线程处理逻辑应该在这个函数中实现。以下是创建自定义线程类的步骤:
- 创建自定义线程类
首先,创建一个QThread的子类。例如,创建一个名为MyThread
的自定义线程类:
#include <QThread> class MyThread : public QThread { Q_OBJECT public: explicit MyThread(QObject *parent = nullptr); protected: void run() override; // 重写run()函数 };
- 重写**
run()
**函数
在MyThread
类中,重写run()
函数以实现线程的处理逻辑。例如:
void MyThread::run() { // 线程处理逻辑,例如执行耗时操作、文件读写等 for (int i = 0; i < 100; ++i) { // 执行操作... } }
- 实例化自定义线程类并启动线程
在应用程序中,可以通过实例化自定义线程类并调用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对象移动到新线程的步骤:
- 实例化QThread和worker对象
在应用程序中,实例化一个QThread对象和一个worker对象:
QThread *thread = new QThread(); MyWorker *worker = new MyWorker();
- 将worker对象移动到新线程
使用moveToThread()
函数将worker对象移动到新线程:
worker->moveToThread(thread);
- 连接信号与槽
使用信号与槽连接线程和worker对象。例如,当线程启动时,调用worker对象的process()
函数:
connect(thread, &QThread::started, worker, &MyWorker::process);
- 也可以连接其他信号,如
finished()
信号和progressChanged
信号。 - 启动线程
调用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