Qt5.14.2 Qt多线程实战演练,全面掌握线程同步和线程池最佳实践

多线程编程是每个开发者必须掌握的基本能力之一。在上一篇文章中,我们学习了Qt多线程编程的理论知识。本文将切入实战,提供多个案例代码,帮助你彻底掌握Qt的多线程编程实践技巧。

案例1: 使用QThread执行耗时任务

这个案例演示了如何通过继承QThread和重写run()函数,在子线程中执行耗时操作,避免阻塞UI线程。

复制代码
// WorkerThread.h
#include <QThread>

class WorkerThread : public QThread
{
    Q_OBJECT

public:
    WorkerThread() {}
    
protected:
    void run() override {
        // 耗时操作
        for(int i=0; i<1000000000; ++i) {
            // 一些复杂的计算
        }
        
        // 通知UI线程任务完成
        emit finished();
    }
    
signals:
    void finished();
};

// main.cpp
#include <QApplication>
#include "WorkerThread.h"

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    
    // 创建工作线程
    WorkerThread* worker = new WorkerThread;
    
    // 连接信号与槽
    QObject::connect(worker, &WorkerThread::finished, [&](){
        qDebug() << "Task finished";
    });
    
    // 启动线程
    worker->start();
    
    w.show();
    return a.exec();
}

在这个例子中,我们定义了WorkerThread类继承自QThread, 并在run()函数中执行模拟的耗时操作。当耗时操作完成后,会发出finished()信号通知UI线程。在主线程中,我们创建WorkerThread实例并启动线程,连接finished()信号到一个Lambda表达式,这样就能在UI线程中安全地更新界面状态。

案例2: 使用QThreadPool执行批量任务

下面这个示例展示了如何利用QThreadPool快速并行执行大量的小任务,充分利用CPU的多核特性。

复制代码
// WorkerTask.h 
#include <QRunnable>

class WorkerTask : public QRunnable
{
public:
    WorkerTask(int num) : m_num(num) {}
    
    void run() override {
        // 模拟耗时操作
        for(int i=0; i<1000000; ++i) {
            // ...
        }
        
        qDebug() << "Task" << m_num << "completed in thread" << QThread::currentThreadId();
    }
    
private:
    int m_num;
};

// main.cpp
#include <QThreadPool>
#include "WorkerTask.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    
    // 获取线程池实例
    QThreadPool* pool = QThreadPool::globalInstance();
    
    // 设置最大线程数为理想值
    pool->setMaxThreadCount(QThread::idealThreadCount());
    
    // 投递大量小任务
    for(int i=0; i<1000; ++i) {
        WorkerTask* task = new WorkerTask(i);
        pool->start(task);
    }
    
    return a.exec();
}

在这个例子中,我们定义了WorkerTask类继承自QRunnable,它的run()函数用于模拟耗时任务。在主线程中,我们首先获取了QThreadPool的全局实例,设置最大线程数为系统理想线程数,然后循环构建1000个WorkerTask实例并通过pool->start()投递给线程池并行执行。

由于使用了线程池避免了频繁创建销毁线程的开销,所有任务能够较为高效地利用系统多核资源并行执行。当线程池中所有线程都处于繁忙状态时,新投递的任务会进入队列排队等待,所以我们要注意控制队列长度,避免性能反而下降。

案例3: 任务队列控制示例

这个示例演示了如何通过监控线程池的任务队列长度,对任务投递策略进行动态调整,防止队列过长导致性能下降。

复制代码
// ThreadPoolManager.h
#include <QThreadPool>

class ThreadPoolManager : public QObject
{
    Q_OBJECT
    
public:
    static ThreadPoolManager* instance(); 
    
    QThreadPool* threadPool() const { return m_threadPool; }
    
    void enqueueTask(QRunnable* task, bool isHighPriority=false);
    
private:
    explicit ThreadPoolManager(QObject* parent=nullptr);
    ~ThreadPoolManager();
    
    QThreadPool* m_threadPool;
    int m_maxThreadCount;
};

// ThreadPoolManager.cpp
#include "ThreadPoolManager.h"

ThreadPoolManager* ThreadPoolManager::instance()
{
    static ThreadPoolManager manager;
    return &manager;
}

ThreadPoolManager::ThreadPoolManager(QObject* parent) 
    : QObject(parent), 
      m_threadPool(QThreadPool::globalInstance()),
      m_maxThreadCount(QThread::idealThreadCount())
{
    m_threadPool->setMaxThreadCount(m_maxThreadCount);
}

void ThreadPoolManager::enqueueTask(QRunnable* task, bool isHighPriority)
{
    // 如果队列过长,根据任务优先级调整策略
    if (m_threadPool->activeThreadCount() == m_maxThreadCount && 
        m_threadPool->queueLength() > 1000) {
        
        if (isHighPriority) { // 对高优先级任务保留线程 
            m_threadPool->reserveThread();
        } else { // 对低优先级任务延迟投递
            QTimer::singleShot(50, [=]{ 
                enqueueTask(task, false); 
            });
            return;
        }
    }
    
    // 投递任务
    m_threadPool->start(task);
    
    // 根据队列长度动态调整线程池大小
    int qLen = m_threadPool->queueLength();
    if (qLen > 2000) {
        m_threadPool->setMaxThreadCount(m_maxThreadCount + 2);
    } else if (qLen < 500) {
        m_threadPool->setMaxThreadCount(m_maxThreadCount);
    }
}

// 在其他地方使用
#include <QThreadPool>
#include "ThreadPoolManager.h"

WorkerTask* task = new WorkerTask;
ThreadPoolManager::instance()->enqueueTask(task, isHighPriority);

在这个例子中,我们定义了ThreadPoolManager类对QThreadPool的使用进行了封装。enqueueTask()函数在投递任务前会先检查当前线程池的状态,如果所有线程都繁忙且任务队列已经积压超过一定长度,那么就会对高优先级任务调用reserveThread()保留线程资源,对低优先级任务延迟50毫秒后重新投递。

此外,enqueueTask()还会根据任务队列的长度动态调整线程池的最大线程数,以应对突发的任务高峰。当队列长度超过2000时,最大线程数会增加;当队列长度降至500以下。

案例4: 使用QMutex保护临界区

这个例子演示了如何使用QMutex互斥锁来保护一段临界区代码,防止多个线程同时访问。

复制代码
// SharedData.h
#include <QMutex>

class SharedData : public QObject
{
    Q_OBJECT

public:
    SharedData() : m_value(0) {}

    void increase()
    {
        // 获取互斥锁
        QMutexLocker locker(&m_mutex);
        
        // 临界区
        m_value++;
    }

    int value() const
    {
        // 由于只有读操作,无需加锁
        return m_value;
    }

private:
    QMutex m_mutex;
    int m_value;
};

// Worker.h
#include <QThread>
#include "SharedData.h"

class Worker : public QThread
{
    Q_OBJECT

public:
    Worker(SharedData* data) : m_data(data) {}

protected:
    void run() override
    {
        // 每个线程执行100000次增加操作
        for(int i=0; i<100000; ++i)
            m_data->increase();
    }

private:
    SharedData* m_data;
};

// main.cpp
#include <QCoreApplication>
#include "Worker.h"

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

    SharedData data;

    // 创建10个工作线程
    QList<QThread*> threads;
    for(int i=0; i<10; ++i)
    {
        Worker* worker = new Worker(&data);
        worker->start();
        threads.append(worker);
    }

    // 等待所有线程执行完毕
    for(QThread* thread : threads)
        thread->wait();

    qDebug() << "Final value:" << data.value(); // 输出 1000000

    return a.exec();
}

在这个例子中,我们定义了SharedData类,它有一个整型value成员变量和一个QMutex互斥锁m_mutex。increase()函数用于对value进行累加,在执行累加操作的临界区代码前,通过QMutexLocker自动获取互斥锁,退出临界区时自动释放锁。

Worker类继承自QThread,在run()函数中循环100000次调用SharedData::increase()函数。

在主线程中,我们创建了一个SharedData实例,然后创建10个Worker线程共享这个实例。所有线程执行完成后,我们输出SharedData的value值,由于临界区被很好地保护,最终输出的结果是正确的1000000。

如果不使用互斥锁,那么多个线程同时访问value变量,就会出现"丢失更新"的问题,导致最终结果不正确。

案例5: 使用QReadWriteLock实现读写分离

对于有读写分离需求的场景,我们可以使用QReadWriteLock来进一步提高并发性能。下面的例子演示了如何使用QReadWriteLock。

复制代码
// Cache.h  
#include <QReadWriteLock>
#include <QHash>

template<typename Key, typename Value>
class Cache
{
public:
    bool insert(const Key& key, const Value& value)
    {
        QWriteLocker locker(&m_lock); // 写锁
        m_cache.insert(key, value);
        return true;
    }
    
    bool getValue(const Key& key, Value& value)
    {
        QReadLocker locker(&m_lock); // 读锁
        auto it = m_cache.find(key);
        if (it == m_cache.end())
            return false;
        value = it.value();
        return true;
    }

private:
    QHash<Key, Value> m_cache;
    QReadWriteLock m_lock;
};

// 使用示例
Cache<QString, int> cache;
cache.insert("apple", 5);
cache.insert("banana", 8);

int value;
if (cache.getValue("apple", value))
    qDebug() << value; // 输出5

在这个例子中,我们定义了一个简单的键值对缓存Cache类。它内部使用QHash存储数据,使用QReadWriteLock控制数据访问。

  • 写操作insert()使用QWriteLocker获取独占写锁,确保线程安全地插入或修改数据。
  • 读操作getValue()只需使用QReadLocker获取共享读锁,因为只读不修改数据,所以允许多个线程同时读取。

使用读写锁的主要优势是,在存在并发读取的情况下,能够最大限度地提高并发性能。多个读线程可以同时访问共享数据而无需等待,只有存在写操作时才会阻塞所有读写线程。

这种细粒度的锁机制可以很好地应用于读多写少的场景,充分利用多核CPU的计算能力,比如数据缓存、文件缓存等领域。开发者可以根据实际需求,权衡性能与线程安全,合理选择不同的线程同步原语。

通过这几个实战案例,相信你已经对Qt的多线程编程有了更深入的理解。无论是创建工作线程执行耗时任务,还是使用线程池高效管理异步任务,亦或是用锁机制保护共享数据,Qt为我们提供了全面的多线程支持。掌握了这些技能,就能更好地开发出高性能、稳定可靠的应用程序。

相关推荐
用户805533698031 天前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner1 天前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
小bo波6 天前
使用Thread子类创建线程 VS 使用Runnable接口创建线程的区别
java·多线程·thread·并发编程·runnable
Quz6 天前
QML Hello World 入门示例
qt
xcyxiner9 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner10 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner10 天前
DicomViewer (添加模型类)3
qt
xcyxiner11 天前
DicomViewer (目录调整) 2
qt
xcyxiner11 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
LDR00613 天前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言