【Qt 线程】探索Qt线程编程的奥秘:多角度深入剖析(一)https://developer.aliyun.com/article/1464088
四、Qt线程安全与同步机制(Thread Safety and Synchronization in Qt)
在多线程编程中,线程安全和同步是非常重要的概念。当多个线程同时访问共享资源时,可能会引发竞争条件(race conditions),导致程序行为不确定或出现错误。为了确保线程安全,需要使用同步机制来保护共享资源。
Qt提供了多种线程安全与同步机制,可以在多线程编程中使用。
4.1 QMutex(互斥锁)
互斥锁(mutex)是一种同步原语,用于保护对共享资源的访问。QMutex类提供了一个互斥锁,可以在Qt应用程序中使用。
以下是使用QMutex保护共享资源的一个示例:
#include <QMutex> class SharedData { public: void setData(int data) { m_mutex.lock(); // 加锁 m_data = data; m_mutex.unlock(); // 解锁 } int getData() { int result; m_mutex.lock(); // 加锁 result = m_data; m_mutex.unlock(); // 解锁 return result; } private: QMutex m_mutex; // 互斥锁 int m_data; // 共享数据 };
在这个示例中,SharedData
类包含一个共享的整数数据和一个互斥锁。setData()
和getData()
函数在访问共享数据时使用互斥锁来保护共享资源。
在实际应用中,可以使用QMutex的lock()
和unlock()
函数保护对共享资源的访问。当一个线程锁定互斥锁时,其他试图锁定该互斥锁的线程将被阻塞,直到互斥锁被解锁。这可以防止多个线程同时访问共享资源,从而确保线程安全。
4.2 QMutexLocker(互斥锁管理器)
为了简化互斥锁的使用和防止死锁,Qt提供了QMutexLocker类,它是一个方便的RAII(Resource Acquisition Is Initialization)风格的互斥锁管理器。当创建一个QMutexLocker对象时,它会自动锁定给定的互斥锁。当QMutexLocker对象超出其作用域或被删除时,它会自动解锁互斥锁。
以下是使用QMutexLocker保护共享资源的一个示例:
#include <QMutex> #include <QMutexLocker> class SharedData { public: void setData(int data) { QMutexLocker locker(&m_mutex); // 加锁 m_data = data; // locker对象超出作用域时自动解锁 } int getData() { int result; QMutexLocker locker(&m_mutex); // 加锁 result = m_data; // locker对象超出作用域时自动解锁 return result; } private: QMutex m_mutex; // 互斥锁 int m_data; // 共享数据 };
在这个示例中,SharedData
类的实现与前一个示例相似,但使用了QMutexLocker来简化互斥锁的使用。QMutexLocker可以确保在函数返回或异常抛出时互斥锁始终被解锁,从而避免死锁。
4.3 QSemaphore(信号量)
信号量是另一种同步原语,用于控制对共享资源的并发访问。QSemaphore类提供了一个信号量,可以在Qt应用程序中使用。信号量管理一个资源计数器,当线程请求资源时,计数器减1;当线程释放资源时,计数器加1。如果计数器为0,请求资源的线程将阻塞,直到有其他线程释放资源。
以下是使用QSemaphore保护有限数量的共享资源的一个示例:
#include <QSemaphore> class SharedResources { public: SharedResources(int resourcesCount) : m_semaphore(resourcesCount) // 初始化信号量 { } void acquireResource() { m_semaphore.acquire(); // 请求资源 } void releaseResource() { m_semaphore.release(); // 释放资源 } private: QSemaphore m_semaphore; // 信号量 };
在这个示例中,SharedResources
类包含一个信号量,用于保护有限数量的共享资源。线程可以通过调用acquireResource()
和releaseResource()
函数来请求和释放资源。如果所有资源都被占用,请求资源的线程将阻塞,直到有其他线程释放资源。
4.4 QReadWriteLock(读写锁)
读写锁是一种特殊类型的锁,允许多个线程同时读共享资源,但只允许一个线程在任何时候写共享资源。QReadWriteLock类提供了一个读写锁,可以在Qt应用程序中使用。读写锁的使用可以提高程序的性能,特别是在读操作远多于写操作的情况下。
以下是使用QReadWriteLock保护共享资源的一个示例:
#include <QReadWriteLock> class SharedData { public: void setData(int data) { m_lock.lockForWrite(); // 获取写锁 m_data = data; m_lock.unlock(); // 释放锁 } int getData() { int result; m_lock.lockForRead(); // 获取读锁 result = m_data; m_lock.unlock(); // 释放锁 return result; } private: QReadWriteLock m_lock; // 读写锁 int m_data; // 共享数据 };
在这个示例中,SharedData
类包含一个共享的整数数据和一个读写锁。setData()
函数在访问共享数据时获取写锁,而getData()
函数在访问共享数据时获取读锁。这可以确保在写入数据时不会有其他线程读取或写入数据,同时允许多个线程同时读取数据。
与QMutex类似,Qt还提供了一个RAII风格的读写锁管理器QReadLocker和QWriteLocker,用于简化读写锁的使用。当创建一个QReadLocker或QWriteLocker对象时,它会自动锁定给定的读写锁以进行读或写操作。当QReadLocker或QWriteLocker对象超出其作用域或被删除时,它会自动解锁读写锁。
4.5 QAtomic 类
QAtomic 类是 Qt 提供的用于原子操作的线程安全类。原子操作是一种不需要加锁的操作,因此可以提高性能。QAtomic 类包括:
- QAtomicInt:用于原子整数操作的类。
- QAtomicPointer:用于原子指针操作的类。
以下是使用 QAtomicInt 的一个示例:
#include <QAtomicInt> class Counter { public: void increment() { m_count.ref(); // 原子地递增计数器 } void decrement() { m_count.deref(); // 原子地递减计数器 } int getCount() const { return m_count.load(); // 原子地加载计数器值 } private: QAtomicInt m_count; // 原子整数 };
在这个示例中,Counter
类包含一个原子整数 m_count
。increment()
和 decrement()
函数分别使用 ref()
和 deref()
方法原子地递增和递减计数器。getCount()
函数使用 load()
方法原子地加载计数器的值。
使用 QAtomic 类可以避免互斥锁带来的性能开销,但请注意,原子操作并不能解决所有线程安全问题。在使用 QAtomic 类时,仍然需要关注线程安全和同步问题,确保程序正确运行。
4.6 QThreadStorage 类
QThreadStorage 类是 Qt 提供的线程局部存储(Thread Local Storage, TLS)工具。线程局部存储是一种特殊的存储机制,每个线程拥有自己的存储空间,线程之间的数据是隔离的。使用 QThreadStorage 可以避免共享资源的竞争问题,提高线程安全性和性能。
以下是使用 QThreadStorage 的一个示例:
#include <QThread> #include <QThreadStorage> class MyThread : public QThread { public: void run() override { if (!m_localData.hasLocalData()) // 检查当前线程是否有局部数据 { m_localData.setLocalData(new int(0)); // 为当前线程分配局部数据 } int *localValue = m_localData.localData(); // 获取当前线程的局部数据 for (int i = 0; i < 10; ++i) { ++(*localValue); qDebug() << "Thread" << currentThreadId() << "local value:" << *localValue; } delete localValue; // 清理局部数据 m_localData.setLocalData(nullptr); } private: static QThreadStorage<int *> m_localData; // 线程局部存储 }; QThreadStorage<int *> MyThread::m_localData; // 初始化静态成员变量
在这个示例中,MyThread
类继承自 QThread
类。每个 MyThread
实例代表一个独立的线程。run()
函数是线程的主要执行方法。在 run()
函数中,我们使用 QThreadStorage
类的 hasLocalData()
、setLocalData()
和 localData()
方法来管理线程的局部数据。这些方法操作的数据仅在当前线程内可见,线程之间的数据是隔离的。
使用 QThreadStorage 可以简化多线程编程,提高线程安全性和性能。但请注意,线程局部存储不适用于所有场景,特别是当需要在线程之间共享数据时。在使用 QThreadStorage 时,请确保线程安全和同步问题得到妥善处理。
五、Qt线程编程方法三:使用QtConcurrent框架(Using QtConcurrent Framework)
QtConcurrent框架为并发编程提供了一个高级接口,它允许您简化多线程应用程序的开发过程,特别是在执行一些可以并行化的任务时。QtConcurrent框架自动管理线程创建、分配和回收,让您专注于任务逻辑。
5.1 QtConcurrent框架简介(Introduction to QtConcurrent Framework)
QtConcurrent框架与QFuture和QFutureWatcher类配合使用,使您能够在主线程中轻松监视任务的进度和结果。
QtConcurrent框架是Qt库的一部分,为并发编程提供了一个高级抽象。它允许您简化多线程应用程序的开发过程,特别是在执行一些可以并行化的任务时。QtConcurrent框架自动管理线程创建、分配和回收,让您专注于任务逻辑。以下是QtConcurrent框架的主要特点:
- 简化线程管理:QtConcurrent框架通过自动管理线程池来简化线程管理。您无需手动创建和销毁线程,而只需关注任务的逻辑。这减轻了开发人员的负担,降低了出错的可能性。
- 函数式编程风格:QtConcurrent框架采用函数式编程风格,使得代码更简洁、易于理解。您可以将任务表示为纯函数,然后将其传递给QtConcurrent来执行。
- 支持容器操作:QtConcurrent框架提供了一系列并行容器操作,如map、mapped和filtered等。这些操作可以在多个线程上同时执行,以提高处理速度。这些操作也支持自定义的谓词和转换函数,使得代码具有更好的灵活性。
- 与QFuture和QFutureWatcher配合使用:QtConcurrent框架与QFuture和QFutureWatcher类配合使用,使您能够在主线程中轻松监视任务的进度和结果。QFuture封装了任务的结果,而QFutureWatcher用于监视任务的状态变化,并发出相应的信号。
- 可扩展性:QtConcurrent框架的设计具有很好的可扩展性。随着硬件资源的增加,例如更多的处理器核心,QtConcurrent框架可以自动利用这些资源来提高程序的性能。
QtConcurrent框架主要包括以下几个部分:
- QtConcurrent::run:用于启动一个可以在后台线程中运行的函数。
- QtConcurrent::map:用于对一个容器中的所有元素应用一个函数。
- QtConcurrent::mapped:用于创建一个新的容器,其中包含将一个函数应用于原始容器中的所有元素所得到的结果。
- QtConcurrent::filtered:用于创建一个新的容器,其中包含满足给定谓词的原始容器中的元素。
5.2 使用QtConcurrent实现并行任务(Implementing Parallel Tasks with QtConcurrent)
在这一节中,我们将介绍如何使用QtConcurrent实现并行任务。
5.2.1 使用QtConcurrent::run启动后台任务
QtConcurrent::run用于启动一个可以在后台线程中运行的函数。以下是一个简单的使用QtConcurrent::run启动后台任务的示例:
#include <QCoreApplication> #include <QDebug> #include <QtConcurrent/QtConcurrent> int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); // 定义一个简单的任务 auto task = []() { qDebug() << "Task started in thread" << QThread::currentThread(); QThread::sleep(3); qDebug() << "Task finished in thread" << QThread::currentThread(); }; // 使用QtConcurrent::run启动任务 QFuture<void> future = QtConcurrent::run(task); qDebug() << "Task started in main thread" << QThread::currentThread(); // 等待任务完成 future.waitForFinished(); qDebug() << "Task finished in main thread" << QThread::currentThread(); return a.exec(); }
在这个示例中,我们定义了一个简单的任务,然后使用QtConcurrent::run在后台线程中启动它。QFuture对象用于表示任务的结果,可以用来检查任务是否完成,或等待任务完成。
5.2.2 使用QtConcurrent::map和QtConcurrent::mapped对容器中的元素应用函数
QtConcurrent::map用于对一个容器中的所有元素应用一个函数。这个操作将在多个线程中并行执行,以提高处理速度。以下是一个使用QtConcurrent::map的示例:
#include <QCoreApplication> #include <QDebug> #include <QVector> #include <QtConcurrent/QtConcurrent> void multiplyByTwo(int &value) { value *= 2; } int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); QVector<int> vector = {1, 2, 3, 4, 5}; // 使用QtConcurrent::map对容器中的所有元素应用函数multiplyByTwo QFuture<void> future = QtConcurrent::map(vector, multiplyByTwo); // 等待任务完成 future.waitForFinished(); qDebug() << "Result:" << vector; return a.exec(); }
QtConcurrent::mapped用于创建一个新的容器,其中包含将一个函数应用于原始容器中的所有元素所得到的结果。以下是一个使用QtConcurrent::mapped的示例:
#include <QCoreApplication> #include <QDebug> #include <QVector> #include <QtConcurrent/QtConcurrent> int multiplyByTwo(int value) { return value * 2; } int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); QVector<int> vector = {1, 2, 3, 4, 5}; // 使用QtConcurrent::mapped创建一个新的容器,其中包含将函数multiplyByTwo应用于原始容器中的所有元素所得到的结果 QFuture<QVector<int>> future = QtConcurrent::mapped(vector, multiplyByTwo); // 等待任务完成 future.waitForFinished(); QVector<int> result = future.result(); qDebug() << "Result:" << result; return a.exec();
在这个示例中,我们使用QtConcurrent::mapped将multiplyByTwo函数应用于原始容器中的所有元素,然后创建了一个新的容器来存储结果。QFuture对象用于表示任务的结果,可以用来检查任务是否完成,或等待任务完成。
5.3 QtConcurrent实例与分析(Examples and Analysis of QtConcurrent)
以下是使用QtConcurrent::map对一个容器中的所有元素应用一个函数的示例:
#include <QCoreApplication> #include <QDebug> #include <QVector> #include <QtConcurrent/QtConcurrent> void multiplyByTwo(int &value) { value *= 2; } int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); QVector<int> vector = {1, 2, 3, 4, 5}; // 使用QtConcurrent::map对容器中的所有元素应用函数multiplyByTwo QFuture<void> future = QtConcurrent::map(vector, multiplyByTwo); // 等待任务完成 future.waitForFinished(); qDebug() << "Result:" << vector; return a.exec(); }
在这个示例中,我们使用QtConcurrent::map将multiplyByTwo函数应用于一个整数向量中的所有元素。这个操作将在多个线程中并行执行,以提高处理速度。当任务完成后,我们输出结果向量。
5.3.1 实例:使用QtConcurrent计算文件的MD5值
在这个示例中,我们将使用QtConcurrent计算多个文件的MD5值。我们将使用QtConcurrent::mapped和QtConcurrent::run来完成这个任务。
#include <QCoreApplication> #include <QDebug> #include <QDir> #include <QCryptographicHash> #include <QFile> #include <QtConcurrent/QtConcurrent> // 定义一个函数来计算文件的MD5值 QByteArray fileMd5(const QString &filePath) { QFile file(filePath); if (!file.open(QIODevice::ReadOnly)) { qWarning() << "Failed to open file" << filePath; return QByteArray(); } QCryptographicHash hash(QCryptographicHash::Md5); if (hash.addData(&file)) { return hash.result().toHex(); } else { qWarning() << "Failed to compute MD5 for file" << filePath; return QByteArray(); } } int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); // 获取要计算MD5值的文件列表 QStringList fileList = QDir("/path/to/your/files").entryList(QDir::Files); // 使用QtConcurrent::mapped并行计算文件的MD5值 QFuture<QByteArray> future = QtConcurrent::mapped(fileList, fileMd5); // 等待任务完成 future.waitForFinished(); // 输出计算结果 for (int i = 0; i < fileList.size(); ++i) { qDebug() << "File:" << fileList[i] << "MD5:" << future.resultAt(i); } return a.exec(); }
在这个示例中,我们首先获取要计算MD5值的文件列表,然后使用QtConcurrent::mapped并行计算文件的MD5值。最后,我们输出计算结果。
5.3.2 分析
通过使用QtConcurrent框架,我们可以简化并行任务的实现。在这个示例中,我们无需手动管理线程池,而只需关注任务本身。QtConcurrent自动管理线程创建、分配和回收,减轻了开发人员的负担。
此外,QtConcurrent::mapped函数可以将任务拆分为多个子任务,并在多个线程上并行执行。这样可以充分利用多核处理器的性能,提高计算速度。
然而,QtConcurrent框架并不适用于所有场景。在某些情况下,如任务之间有复杂的依赖关系,或需要精确控制线程的执行顺序时,使用QThread类和信号槽机制可能更合适。
在实际开发中,您需要根据具体需求选择合适的线程编程方法。在适用的场景下,QtConcurrent框架可以大大简化多
六、Qt线程编程方法四:使用 QThreadPool 和 QRunnable
QThreadPool 是 Qt 提供的一个线程池类,可以用来管理和回收线程资源。与创建和销毁 QThread 实例相比,使用 QThreadPool 可以减少线程创建和销毁的开销,提高程序性能。
QRunnable 是一个抽象类,用于封装可以在线程池中执行的任务。通过继承 QRunnable 并实现其 run() 函数,您可以定义自己的任务类。
6.1 QThreadPool 简介(Introduction to QThreadPool)
QThreadPool 类可以创建和管理一组线程,以并发执行任务。线程池可以自动管理线程的创建和销毁,以及任务的分配。当任务完成时,线程会返回到线程池,等待下一个任务。这可以减少线程创建和销毁的开销,提高程序性能。
QThreadPool 提供了一些实用的功能,如设置线程池的最大线程数、等待所有任务完成以及取消所有未执行的任务等。
QThreadPool 类是一个线程池类,用于管理并发执行的任务。线程池在内部维护一组工作线程,这些线程可重复利用以减少线程创建和销毁的开销。QThreadPool 提供以下功能:
- 自动创建和销毁线程:线程池会根据任务的数量和负载自动创建新的线程,当线程空闲一段时间后,线程池会自动销毁线程,释放资源。
- 限制最大线程数:线程池可以设置最大线程数,以防止线程数量过多导致系统资源耗尽。当线程池中的线程达到最大值时,新提交的任务将等待,直到有空闲线程可用。
- 线程优先级:QThreadPool 允许为任务设置优先级,优先级较高的任务会优先分配给空闲线程执行。
- 任务排队策略:线程池可以设置任务排队策略,例如先进先出(FIFO)或后进先出(LIFO)等。这可以根据任务特性和应用需求进行调整,以实现更好的性能。
- 全局线程池实例:QThreadPool 提供一个全局线程池实例,可以通过 QThreadPool::globalInstance() 函数获取。全局线程池实例适用于大多数场景,简化了线程池的使用。
通过使用 QThreadPool,您可以更有效地管理线程资源,提高多线程程序的性能。同时,它还简化了多线程编程,让您能够专注于任务逻辑,而无需关注线程的创建、销毁和调度。
6.2 创建自定义 QRunnable(Creating Custom QRunnable)
要在线程池中执行任务,需要创建一个继承自 QRunnable 的自定义类,并重写其 run() 函数。以下是一个简单的自定义 QRunnable 类示例:
#include <QRunnable> #include <QDebug> #include <QThread> class MyRunnable : public QRunnable { public: void run() override { qDebug() << "Running task in thread" << QThread::currentThread(); // 在这里执行任务逻辑 } };
在这个示例中,我们创建了一个名为 MyRunnable 的自定义任务类,并在 run() 函数中输出了当前线程信息。您可以在 run() 函数中添加您需要执行的任务逻辑。
6.2.1 传递参数给 QRunnable
要将参数传递给自定义 QRunnable 类,您可以在类中添加成员变量和构造函数。以下是一个传递参数给 QRunnable 的示例:
#include <QRunnable> #include <QDebug> #include <QThread> class MyRunnable : public QRunnable { public: explicit MyRunnable(int value) : m_value(value) { } void run() override { qDebug() << "Running task with value" << m_value << "in thread" << QThread::currentThread(); // 在这里执行任务逻辑 } private: int m_value; };
在这个示例中,我们在 MyRunnable 类中添加了一个整数成员变量 m_value,并通过构造函数将其初始化。这样,我们就可以在创建 MyRunnable 实例时传递参数。
6.2.2 使用信号与槽
尽管 QRunnable 不是 QObject 的子类,但您仍然可以在自定义 QRunnable 类中使用信号与槽。您可以将 QRunnable 类中的信号与槽与其他 QObject 子类的对象连接,以实现线程间的通信。
要在 QRunnable 类中使用信号与槽,您需要将 QRunnable 与 QObject 组合,而不是将 QRunnable 作为 QObject 的子类。以下是一个在 QRunnable 类中使用信号与槽的示例:
#include <QObject> #include <QRunnable> #include <QDebug> #include <QThread> class MyWorker : public QObject { Q_OBJECT public: void process() { qDebug() << "Processing in thread" << QThread::currentThread(); emit finished(); } signals: void finished(); }; class MyRunnable : public QRunnable { public: MyRunnable(MyWorker *worker) : m_worker(worker) { } void run() override { connect(this, &MyRunnable::finished, m_worker, &MyWorker::deleteLater, Qt::DirectConnection); m_worker->process(); emit finished(); } signals: void finished(); private: MyWorker *m_worker; };
在这个示例中,我们创建了一个名为 MyWorker 的 QObject 子类,并在其中定义了一个名为 process() 的槽。然后,我们在 MyRunnable 类中添加了一个 MyWorker 指针,并在 run() 函数中连接信号与槽。这样,我们就可以在不同线程中使用信号与槽进行通信。
6.3 使用 QThreadPool 管理线程(Managing Threads with QThreadPool)
要使用 QThreadPool 管理线程并执行任务,可以按照以下步骤操作:
- 创建一个 QThreadPool 实例。通常情况下,可以使用 QThreadPool 的全局实例,通过 QThreadPool::globalInstance() 函数获取。
- 创建一个自定义 QRunnable 实例。
- 将 QRunnable 实例提交给线程池,线程池会自动选择一个空闲线程来执行任务。
以下是一个使用 QThreadPool 的示例:
#include <QCoreApplication> #include <QThreadPool> #include "MyRunnable.h" int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); // 获取 QThreadPool 的全局实例 QThreadPool *pool = QThreadPool::globalInstance(); // 创建一个自定义 QRunnable 实例 MyRunnable *task = new MyRunnable(); // 将任务提交给线程池 pool->start(task); // 等待所有任务完成 pool->waitForDone(); return a.exec(); }
QThreadPool 提供了一些高级功能,如设置线程优先级、限制最大线程数以及在超时后自动销毁空闲线程等。在这里,我们将介绍这些高级功能的使用方法。
6.3.1 设置线程优先级
当将任务提交给线程池时,可以为其分配一个优先级。优先级可以是 QThread::Priority 枚举值。线程池会根据任务的优先级来决定任务的执行顺序。任务优先级高的会优先执行。
// 创建一个自定义 QRunnable 实例 MyRunnable *highPriorityTask = new MyRunnable(); // 将任务提交给线程池,设置优先级为 QThread::HighPriority pool->start(highPriorityTask, QThread::HighPriority); // 创建另一个自定义 QRunnable 实例 MyRunnable *lowPriorityTask = new MyRunnable(); // 将任务提交给线程池,设置优先级为 QThread::LowPriority pool->start(lowPriorityTask, QThread::LowPriority);
6.3.2 限制最大线程数
您可以通过 QThreadPool::setMaxThreadCount() 函数限制线程池的最大线程数。这对于避免创建过多线程导致系统资源耗尽非常有用。
// 将线程池的最大线程数设置为 4 pool->setMaxThreadCount(4);
6.3.3 自动销毁空闲线程
线程池可以在一段时间后自动销毁空闲线程。要启用此功能,可以使用 QThreadPool::setExpiryTimeout() 函数设置超时时间(以毫秒为单位)。
// 将空闲线程的超时时间设置为 30000 毫秒(30 秒) pool->setExpiryTimeout(30000);
这样,当线程在 30 秒内没有执行任务时,线程池会自动销毁这个线程,以释放系统资源。
【Qt 线程】探索Qt线程编程的奥秘:多角度深入剖析(三)https://developer.aliyun.com/article/1464091