目录
一、背景
多线程是提高应用程序性能和响应速度的常用技术之一,而在 Qt 中实现多线程也变得异常简单和高效。本文将对 Qt 中实现多线程的几种常用方法进行介绍,并结合示例程序展示其实际应用。
二、线程基础
(1)使用 QThread 类
QThread 类是 Qt 中实现多线程的基础类之一,通过继承 QThread 类并重写其 run() 函数可以实现自定义线程逻辑。
线程类:
#ifndef WORKER_H
#define WORKER_H
#include <QThread>
class Worker : public QThread
{
public:
Worker();
void run();
void printFunc();
};
#endif // WORKER_H
#include "Worker.h"
#include <QDebug>
Worker::Worker()
{
}
void Worker::run()
{
qDebug()<<"子线程ThreadID: "<<QThread::currentThreadId();
}
void Worker::printFunc()
{
qDebug()<<"子线程成员函数ThreadID: "<<QThread::currentThreadId();
}
main函数:
#include <iostream>
#include <QDebug>
#include "Worker.h"
using namespace std;
int main()
{
Worker w;
w.start();
qDebug()<<"主线程ThreadID: "<<QThread::currentThreadId();
w.printFunc();
while (1)
{
}
return 0;
}
结果展示:
结果分析:
- 主线程和子线程执行的顺序不确定,偶尔主线程在前,偶尔子线程在前。
- 子线程类的成员函数包括槽函数是运行在主线程当中的,只有run()函数运行在子线程中。
- 如果在run()函数中调用子线程类成员函数,那么该成员函数运行在子线程中。
#include "Worker.h"
#include <QDebug>
Worker::Worker()
{
}
void Worker::run()
{
qDebug()<<"子线程ThreadID: "<<QThread::currentThreadId();
printFunc();
}
void Worker::printFunc()
{
qDebug()<<"子线程成员函数ThreadID: "<<QThread::currentThreadId();
// emit doTask();
}
结果展示:
(2)使用 moveToThread()
moveToThread() 是 Qt 中用于将对象移动到另一个线程的方法。通过调用 moveToThread() 函数,可以将一个 QObject 对象从当前线程移动到另一个线程中,从而实现对象在新线程中执行特定的任务。
在多线程编程中,通常会使用 moveToThread() 方法来将耗时的任务或需要在单独线程中执行的逻辑移动到单独的线程中,以避免阻塞主线程(通常是 GUI 线程)的执行。
线程类:
#ifndef WORKER_H
#define WORKER_H
#include <QObject>
class Worker : public QObject
{
Q_OBJECT
public:
Worker();
void printFunc();
public slots:
void doWork();
void doWork2();
void doWork3();
signals:
void testdoWork3();
};
#endif // WORKER_H
#include "Worker.h"
#include <QDebug>
#include <QThread>
Worker::Worker()
{
}
void Worker::printFunc()
{
qDebug() << "成员函数ThreadID:"<<QThread::currentThreadId();
}
void Worker::doWork()
{
qDebug() << "doWork ThreadID:"<<QThread::currentThreadId();
}
void Worker::doWork2()
{
qDebug() << "doWork2 ThreadID:"<<QThread::currentThreadId();
}
void Worker::doWork3()
{
qDebug() << "doWork3 ThreadID:"<<QThread::currentThreadId();
}
main函数:
#include "mainwindow.h"
#include <QApplication>
#include "Worker.h"
#include <QDebug>
#include <QThread>
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
//MainWindow w;
//w.show();
Worker worker;
QThread thread;
worker.moveToThread(&thread);
QObject::connect(&thread, &QThread::started, &worker, &Worker::doWork); //第一槽函数
QObject::connect(&thread, &QThread::started, &worker, &Worker::doWork2); //第二槽函数
QObject::connect(&worker, &Worker::testdoWork3, &worker, &Worker::doWork3); //第三槽函数
//启动线程
thread.start();
//调用成员数
worker.printFunc();
//发送自定义信号
emit worker.testdoWork3();
while (1) {
}
return a.exec();
}
结果展示:
结果分析:
- 槽函数无论是线程的信号触发还是自定义信号触发,槽函数都在新线程里运行。
- 成员函数和主函数运行在主线程当中。
(3)QThread常用函数及注意事项
以下是一些常用的QThread函数:
- start(): 启动线程,使线程进入运行状态,调用线程的run()方法。
- run(): 线程的执行函数,需要在该函数中编写线程所需执行的任务。
- quit(): 终止线程的事件循环,在下一个事件处理周期结束时退出线程。
- wait(): 阻塞当前线程,直到线程执行完成或超时。
- finished(): 在线程执行完成时发出信号。
- terminate(): 强制终止线程的执行,不推荐使用,可能导致资源泄漏和未定义行为。
- isRunning(): 判断线程是否正在运行。
- currentThreadId(): 返回当前线程的ID。
- yieldCurrentThread(): 释放当前线程的时间片,允许其他线程执行。
- setPriority(): 设置线程优先级。
- msleep(): 让当前线程休眠指定的毫秒数。
在使用QThread类中的常用函数时,有一些注意事项需要注意:
- start()函数: 调用start()函数启动线程时,会自动调用线程对象的run()方法。不要直接调用run()方法来启动线程,应该使用start()函数。
- wait()函数: wait()函数会阻塞当前线程,直到线程执行完成。在调用wait()函数时需要确保不会发生死锁的情况,避免主线程和子线程相互等待对方执行完成而无法继续。
- terminate()函数: 调用terminate()函数会强制终止线程,这样可能会导致资源未能正确释放,造成内存泄漏等问题。因此应该尽量避免使用terminate()函数,而是通过设置标志让线程自行退出。
- quit()函数: quit()函数用于终止线程的事件循环,通常与exec()函数一起使用。在需要结束线程事件循环时,可以调用quit()函数。
- finished信号: 当线程执行完成时会发出finished信号,可以连接这个信号来处理线程执行完成后的操作。
- yieldCurrentThread()函数: yieldCurrentThread()函数用于让当前线程让出时间片,让其他线程有机会执行。使用时应该注意避免过多的调用,否则会影响程序性能。
(4)两种方式的缺点
QThread 方式:
使用场景:
- 当需要创建一个独立的线程来执行某个任务,且需要对线程的整个生命周期进行管理时,适合使用 QThread 方式。
- 当任务逻辑相对简单或独立,不需要频繁地进行线程间通信时,可以选择使用 QThread 方式。
优点:
- 可以直接控制线程的生命周期,包括启动、停止、等待线程退出等。
- 适合单一任务的线程处理,结构相对清晰易懂。
- 相对直观,可以比较容易理解和使用。
缺点:
- 需要手动管理线程之间的通信和数据共享,容易引入线程安全问题。
- 繁琐的线程管理和同步机制可能增加代码复杂度和风险。
moveToThread() 方式:
使用场景:
- 当需要将一个 QObject 对象移动到指定的线程中执行任务,或者需要多个对象在同一线程中协同工作时,适合使用 moveToThread() 方式。
- 当需要灵活地控制对象和线程之间的关系,进行复杂的线程间通信时,可以选择使用 moveToThread() 方式。
优点:
- 可以利用信号和槽机制方便地实现对象在不同线程中的通信。
- 可以更灵活地管理对象和线程的关系,避免直接操作线程带来的问题。
- 适合处理复杂的多线程通信和任务分发。
缺点:
- 无法直接控制线程的启动和停止,线程的生命周期由对象决定,可能使得线程管理稍显复杂。
- 对对象的线程移动可能引入一些额外的开销,需要谨慎设计线程之间的交互逻辑。
总结:
选择使用 QThread 或 moveToThread() 方式创建线程取决于具体需求和情况。可以根据以下原则进行选择:
- 如果需要独立管理整个线程的生命周期、简单的多线程操作,并且不涉及复杂的线程间通信,可以选择 QThread 方式。
- 如果需要灵活地管理对象与线程之间的关系、复杂的多线程通信和任务分发,可以选择 moveToThread() 方式。
- 综上所述,根据项目需求、任务复杂度和开发方便性来选择适合的创建线程方式。
三、线程的同步与互斥
(1)QMutex / QMutexLocker
QMutex 是 Qt 中用于实现互斥锁的类,用于保证在多线程程序中访问共享资源的互斥性。它提供了两个基本操作:lock() 和 unlock(),分别用于加锁和解锁。
QMutexLocker 是 QMutex 的 RAII 风格封装,可以自动释放锁资源,避免忘记解锁而导致的死锁情况。QMutexLocker 在创建时会自动调用 QMutex 的 lock() 方法,析构时会自动调用 QMutex 的 unlock() 方法。因此使用 QMutexLocker 可以大大减少忘记解锁的情况。
#include <QCoreApplication>
#include <QThread>
#include <QDebug>
#include <QMutex>
// 定义共享资源
int sharedValue = 0;
QMutex mutex;
// 定义一个线程类
class MyThread : public QThread
{
public:
void run() override {
for(int i = 0; i < 5; i++) {
mutex.lock(); // 加锁
sharedValue++; // 访问共享资源
qDebug() << "Thread ID: " << QThread::currentThreadId() << " - Shared Value: " << sharedValue;
msleep(1000); // 线程休眠1秒
mutex.unlock(); // 解锁
}
}
};
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
MyThread thread1;
MyThread thread2;
thread1.start();
thread2.start();
thread1.wait();
thread2.wait();
qDebug() << "Final Shared Value: " << sharedValue;
return a.exec();
}
结果展示:
加锁:
未加锁:
结果分析: 明显看出在未加锁情况下对临界资源的访问出现混乱的结果。
使用QMutexLocker会更加方便,更加简洁一些,原理与QMutex一致。涉及代码如下:
void run() override {
for(int i = 0; i < 5; i++) {
QMutexLocker locker(&mutex); // 创建 QMutexLocker 对象并传递 QMutex 对象
sharedValue++; // 访问共享资源
qDebug() << "Thread ID: " << QThread::currentThreadId() << " - Shared Value: " << sharedValue;
msleep(1000); // 线程休眠1秒
}
}
(2)QSemaphore
QSemaphore 是 Qt 中用于实现信号量的类,用于控制对共享资源的访问数量。它可以用来限制同时访问某一资源的线程数量,也可以用于线程之间的同步。QSemaphore 可以被获取和释放,当信号量的值为正时,线程可以获得该信号量;当信号量的值为零时,线程将被阻塞,直到有线程释放信号量。通过获取和释放信号量,可以实现线程之间的协调和资源的管理。
#include <QCoreApplication>
#include <QThread>
#include <QDebug>
#include <QSemaphore>
QSemaphore semaphore(2); // 定义能够同时访问资源的线程数量为2的信号量
class MyThread : public QThread // 定义一个线程类
{
public:
void run() override {
if(semaphore.tryAcquire()) { // 尝试获取信号量
qDebug() << "Thread ID: " << QThread::currentThreadId() << " - Acquired Semaphore"; // 输出线程ID和已获取信号量消息
sleep(2); // 线程休眠2秒
qDebug() << "Thread ID: " << QThread::currentThreadId() << " - Releasing Semaphore"; // 输出线程ID和释放信号量消息
semaphore.release(); // 释放信号量
} else {
qDebug() << "Thread ID: " << QThread::currentThreadId() << " - Semaphore not acquired"; // 输出线程ID和未获取信号量消息
}
}
};
int main(int argc, char *argv[]) // 主函数
{
QCoreApplication a(argc, argv); // 创建应用程序对象
MyThread thread1; // 创建线程对象1
MyThread thread2; // 创建线程对象2
MyThread thread3; // 创建线程对象3
thread1.start(); // 启动线程1
thread2.start(); // 启动线程2
thread3.start(); // 启动线程3
thread1.wait(); // 等待线程1结束
thread2.wait(); // 等待线程2结束
thread3.wait(); // 等待线程3结束
return a.exec(); // 执行应用程序事件循环
}
结果展示:
当QSemaphore semaphore(2):
当QSemaphore semaphore(3):
(3)QWaitCondition
QWaitCondition 是 Qt 框架中用于线程间同步的类之一。它允许一个线程等待另一个线程发出信号,从而实现线程间的协调和同步。
在使用 QWaitCondition 时,通常会配合使用 QMutex。QMutex 用于保护共享资源,而 QWaitCondition 则用于在等待某个条件为真时挂起线程,并在条件满足时唤醒线程。
#include <QCoreApplication>
#include <QThread>
#include <QDebug>
#include <QWaitCondition>
#include <QMutex>
#include <QQueue>
QMutex mutex; // 创建一个互斥锁,确保线程安全
QWaitCondition queueNotEmpty; // 创建一个条件变量,表示队列非空
QQueue<int> queue; // 创建一个队列用于存储数据
// 生产者线程
class ProducerThread : public QThread
{
public:
void run() override
{
for (int i = 0; i < 10; ++i) {
// 生产数据并加入队列
{
QMutexLocker locker(&mutex); // 加锁
queue.enqueue(i); // 生产数据并加入队列
qDebug() << "Produced: " << i;
queueNotEmpty.wakeOne(); // 通知消费者队列非空
}
msleep(100); // 休眠一段时间
}
}
};
// 消费者线程
class ConsumerThread : public QThread
{
public:
void run() override
{
for (int i = 0; i < 10; ++i) {
// 检查队列是否为空,如果为空则等待
{
QMutexLocker locker(&mutex); // 加锁
while (queue.isEmpty()) {
queueNotEmpty.wait(&mutex); // 等待条件变量,直到队列非空
}
int value = queue.dequeue(); // 从队列中取出数据
qDebug() << "Consumed: " << value;
}
msleep(200); // 休眠一段时间
}
}
};
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// 创建生产者线程和消费者线程
ProducerThread producer;
ConsumerThread consumer;
// 启动线程
producer.start();
consumer.start();
// 等待线程结束
producer.wait();
consumer.wait();
return a.exec();
}
结果展示:
(4)QReadWriteLock
QReadWriteLock 是 Qt 提供的用于读写操作的锁类,允许多个线程同时读取共享数据,但在写操作时会阻止其他的读取和写入,以确保数据的一致性。
#include <QCoreApplication>
#include <QThread>
#include <QDebug>
#include <QReadWriteLock>
QString sharedData; // 共享数据变量
QReadWriteLock rwLock; // 读写锁
// 读取操作线程
class ReaderThread : public QThread
{
public:
void run() override
{
rwLock.lockForRead(); // 以读取方式加锁
qDebug() << "Read Data: " << sharedData; // 输出读取的数据
rwLock.unlock(); // 释放锁
}
};
// 写入操作线程
class WriterThread : public QThread
{
public:
void run() override
{
rwLock.lockForWrite(); // 以写入方式加锁
sharedData = "Hello, world!"; // 写入数据
qDebug() << "Write Data: " << sharedData; // 输出写入的数据
rwLock.unlock(); // 释放锁
}
};
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// 创建读取线程和写入线程
ReaderThread reader;
WriterThread writer;
// 启动线程
reader.start(); // 启动读取线程
writer.start(); // 启动写入线程
// 等待线程结束
reader.wait(); // 等待读取线程结束
writer.wait(); // 等待写入线程结束
return a.exec();
}
结果展示:
结果分析:
这两种结果都是正常的现象,因为没有控制线程的执行顺序。
四、总结
上述展示了Qt多线程编程的基本知识,与C语言、C++等语言线程大体上一致。线程间的同步和互斥涉及到的知识点大体不差。Qt还可以通过信号和槽连接,在不同线程之间进行通信和同步。Qt多线程编程更需要关注的点是线程的生命周期以及如何更加优雅的退出线程。Qt除了线程还提供了一些模块、线程池等功能和机制来实现并发执行。