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为我们提供了全面的多线程支持。掌握了这些技能,就能更好地开发出高性能、稳定可靠的应用程序。

相关推荐
wn5319 分钟前
【Go - 类型断言】
服务器·开发语言·后端·golang
Hello-Mr.Wang21 分钟前
vue3中开发引导页的方法
开发语言·前端·javascript
救救孩子把24 分钟前
Java基础之IO流
java·开发语言
WG_1725 分钟前
C++多态
开发语言·c++·面试
宇卿.32 分钟前
Java键盘输入语句
java·开发语言
Amo Xiang42 分钟前
2024 Python3.10 系统入门+进阶(十五):文件及目录操作
开发语言·python
friklogff1 小时前
【C#生态园】提升C#开发效率:深入了解自然语言处理库与工具
开发语言·c#·区块链
重生之我在20年代敲代码2 小时前
strncpy函数的使用和模拟实现
c语言·开发语言·c++·经验分享·笔记
爱上语文2 小时前
Springboot的三层架构
java·开发语言·spring boot·后端·spring
编程零零七4 小时前
Python数据分析工具(三):pymssql的用法
开发语言·前端·数据库·python·oracle·数据分析·pymssql