Qt的四种多线程讲解

文章目录

  • Qt 四种多线程
    • QThread: 底层API。带有可选的事件循环
    • QThreadPool和QRunnable:复用线程
    • Qt Concurrent: 高级API
    • WorkerScript: QML中的线程
  • 线程安全和可重入
  • 示例有助于理解

Qt 提供了对线程的支持,包括一组与平台无关的线程类,一个线程安全的发送事件和跨线程的信号-槽关联。
通常有四种方式来实现多线程Qt程序。

Qt 四种多线程

QThread: 底层API。带有可选的事件循环

QThread是Qt中所有线程控制的基础,一个QThread实例代表并控制一个线程。QThread可以直接实例化,也可以被继承使用。实例化一个QThread会提供一个并行的事件循环,并能够在从属线程中调用槽函数。继承或子类化一个QThread并重写run()方法有助于实现高级的线程管理,可以有选择性是否启用事件循环。还可以,使用一个继承于QObject类的实例,通过moveToThread()将该实例移动到创建的新线程中,而后,可以向该实例跨线程发送信号来控制其执行。

QThreadPool和QRunnable:复用线程

QThreadPool(线程池类)是可复用线程的集合。QThreadPool管理和回收线程对象个体以帮助减少多线程程序中的线程创建消耗。每一个Qt应用都有一个全局的QThreadPool对象,可以通过QThreadPool::globalInstance()获取。全局线程池基于CPU核心数维护最优数量的线程。不过,创建一个单独的线程池可以更加明确的管理线程。
如何使用:要使用线程池中的一个线程,需要子类化 QRunnable接口(抽象)类并实现run()方法。然后调用QThreadPool::start(),并传入自定义的QRunnable子类,这会将其加入QThreadPool的run队列中。当有线程可用时,该线程就会执行QRunnable::run()中的代码。不用担心内存释放问题,run()执行完后QThreadPool默认会自动释放传入的QRunnable对象,也可以通过QRunnable::autoDelete()来调整。

Qt Concurrent: 高级API

Qt Concurrent模块的功能包含在QtConcurrent命名空间里,它提供了一些高级函数来处理常用的计算模式:map,filter,和reduce。和QThread不同的是,这些函数不使用底层的原语,如信号量,条件变量。而且,用QtConcurrent写的程序能根据CPU可用核心数自动调整线程数。这个特性使得用它写的程序能够在将来部署到多核机器上时自动缩放。QtConcurrent使用用于并行列表处理的函数式编程风格API,包括用于共享内存系统的MapReduce和FilterReduce实现,以及用于在GUI程序中管理异步计算的类。
其API可分为如下部分:

  • 序列上的并行计算:
    QtConcurrent::map() 自动申请最佳线程调用传入的map函数并行对容器中的每个元素做就地运算,返回一个QFuture只用于状态管理
    QtConcurrent::mapped() 类似于map(),只是不做就地修改,调用时创建拷贝,并且会返回一个QFuture用于处理每一个mapped后的结果
    QtConcurrent::mappedReduced() 类似于mapped(),只是会使用reduce函数将结果整合成一个值,并返回该值
  • 序列上的并行过滤(移除)
    QtConcurrent::filter() 根据传入的filter函数的结果从容器中移除元素,返回一个QFuture只用于状态管理
    QtConcurrent::filtered() 类似于filter(),只是不做就地修改,将需要保留的元素拷贝到新容器,返回一个QFuture序列用于处理每一个filtered后的结果
    QtConcurrent::filteredReduced() 类似于filtered(),只是会使用reduce函数将结果整合成一个值,并返回该值
    上述函数都有一个blocking变体,如blockingMap(),该变体不返回QFuture而是处理后的值,另外该变体会阻塞线程直到得到结果。
  • 单独的线程执行
    QtConcurrent::run() 从线程池中获得一个线程运行传入的函数,返回一个QFuture获取返回值和状态
  • 用于管理的类
    QFuture 表征异步运算的未来将得到的结果
    QFutureIterator QFuture既有java风格的迭代器也有STL风格的迭代器。QFutureIterator提供的是java风格迭代器,更好用。
    QFutureWatcher 使用信号-槽监控QFuture,类似于代理模式,代理QFuture

WorkerScript: QML中的线程

WorkerScript QML类型让JavaScript代码和GUI线程并行执行。每个WorkerScript实例可以被一个.js依附。调用WorkerScript.sendMessage()会让这个.js在一个单独的线程中运行。这个.js执行完后,它会发送一个应答给GUI线程,GUI线程将会调用WorkerScript.onMessage()信号处理器进行处理。
使用WorkerScript 类似于使用一个被移动到另一个线程的QObject对象。

QThread
每一个线程都可以有自己的事件循环。主线程使用QCoreApplication::exec()来开启事件循环(对话框可以使用QDialog::exec()),其他线程使用QThread::exec()来开启事件循环。和QCoreApplication类似,QThread也提供了exit()函数和quit()槽。另外,需要事件循环的非GUI类,QTimer,QTcpSocket, QProcess。如果没有运行事件循环,则事件将无法传送到QObject对象。调用deleteLater()也不会工作。

线程安全和可重入

“安全”意味着,无论其他线程调用该函数的执行状态如何,函数均可产生预期结果。
一个函数可以被多个线程同时并发地安全调用,则该函数为线程安全函数;
何为可重入?一个函数被重入表示这个函数没有执行完成,由于外部因素或内部调用,又一次进入函数执行。一个函数要被重入,只有两种情况:
1. 多个线程同时执行这个函数
2. 函数自身(可能经过多层调用后)调用自身
一个函数被称为可重入的,表示该函数被重入后不会产生不良后果。一个函数要成为可重入的,必须具有以下几个特点:
1. 不使用任何静态或全局的非const变量
2. 不返回任何静态或全局的非const变量的指针
3. 仅依赖于调用方提供的参数
4. 不依赖任何单个资源的锁
5. 不调用任何不可重入的函数
SUSv3 对可重入函数的定义是:函数由多条线程调用时,即便是交叉执行,其效果也与各线程以任意顺序依次调用时一致。
为实现线程安全,一般需要一些线程同步机制,而可重入函数不使用互斥量即可实现线程安全,其秘诀在于直接避免对全局变量或静态变量的使用;需要返回给调用者的任何信息,亦或是需要在对函数的历次调用间加以维护的信息,都存储于由调用者分配的缓冲区内。但并非所有函数都可实现可重入,有些函数必须访问全局数据结构,一些库函数本身就定义为不可重入,要么返回指针来指向函数自身静态分配的空间,要么利用静态存储来对函数历次调用的信息加以维护。

举个例子:函数 asctime() 返回一个指针,指向自身静态分配的缓冲区,其内容为日期和时间字符串。所以它是不可重入的。SUSv3为其定义了可重入“替身”,asctime_r()。该函数多了一个入参,要求由调用者来分配缓冲区,并将缓存区地址传给函数用以返回结果。这使得调用线程可以使用局部(栈)变量来存放函数结果。

一般来说,线程安全是个更大的概念,而可重入则一般只针对函数而言。
线程安全是指在多线程环境下程序运行能够得到正确的结果;可重入函数是指两个或者多个线程同时进入一个函数内部执行,而不会发生错误,该函数本身是线程安全的。但可能出现多个可重入函数分别使用时都是可重入的,线程安全的,但配合使用时,却不一定是线程安全的。也就是说函数可重入却不一定能保证模块是线程安全的。

Qt中对线程安全和可重入也做了总结:
一个线程安全的函数可以同时被多个线程调用,即便是它们使用了共享数据,因为使用了线程同步机制;
一个可重入的函数也可以同时被多个线程调用,但是只能是在每个调用使用自己的数据的情况下。
推而广之,一个可重入的类指的是,只要每个线程使用一个类的不同实例,该类的成员函数可以被多个线程安全地调用;
一个线程安全的类指的是,即使所有的线程使用一个类的相同实例,该类的成员函数仍能被多个线程安全地调用。
所以,如果一个函数没有被标记为线程安全或着可重入的,它不应当被多个线程使用;
如果一个类没有被标记为线程安全或可重入的,则该类的一个特定实例不应当被多个线程访问;
一个线程安全的函数总是可重入的,但一个可重入的函数却不一定总是线程安全的。


示例有助于理解

// QThread 使用条件变量同步
#include 
#include 
#include 
#include 

constexpr int DATA_SIZE = 100;
constexpr int BUFFER_SIZE = 64;
int available;  // 确保生产者不能生产多于消费者BUFFER_SIZE个数据,消费者不会拿到生产者还未生产的数据

char message[BUFFER_SIZE];

QMutex mutex;
QWaitCondition notFull;
QWaitCondition notEmpty;

class Producer : public QThread {
public:
    Producer() {}
    virtual ~Producer() {}

protected:
    void run() override
    {
        for (int i = 0; i < DATA_SIZE; i++) {
            mutex.lock();
            while (available == BUFFER_SIZE) { // 这个例子,也可用if,不过更通用的做法是用while,而且必须,值得思考
                // 条件变量与互斥量之间存在着天然的关联关系
                // 如果共享变量未处于预期状态,线程应在等待条件变量并进入休眠前解锁互斥量(以便其他线程能访问该共享变量)。
                // 当线程因为条件变量的通知而被再度唤醒时,必须对互斥量再次加锁,因为在典型情况下,线程会立即访问共享变量。
                notFull.wait(&mutex); // 简而言之,做了两件事,休眠前解锁互斥量,唤醒时立即加锁互斥量
            }
            mutex.unlock();

            message[i % BUFFER_SIZE] = 'x';  // 有了条件变量的控制,就不用枷锁,就可以对全局变量多线程安全地访问,读写线程可以同时处理该全局变量的不同部分
            fprintf(stdout, "+");

            mutex.lock();
            available++;
            mutex.unlock();
            notEmpty.wakeOne();
        }
    }
};

class Consumer : public QThread {
public:
    Consumer() {}
    virtual ~Consumer() {}

protected:
    void run() override
    {
        for (int i = 0; i < DATA_SIZE; i++) {
            mutex.lock();
            while (available == 0) {
                notEmpty.wait(&mutex);
            }
            mutex.unlock();

//            fprintf(stdout, "%c", message[i % BUFFER_SIZE]);
            fprintf(stdout, "-");

            mutex.lock();
            available--;
            mutex.unlock();
            notFull.wakeOne();
            msleep(10);
        }
    }
};

int main(int argc, char* argv[])
{
    QCoreApplication app(argc, argv);
    Producer producer;
    Consumer consumer;
    producer.start();
    consumer.start();
    producer.wait();  // 类似与pthread_join()
    consumer.wait();
    fprintf(stdout, "****");
    return 0;
}

// QtConcurrent模块 QFuture的一个示例
#include 
#include 

#include 

using namespace QtConcurrent;

int main(int argc, char **argv)
{
    QApplication app(argc, argv);

    const int iterations = 200;

    QVector<int> vector;
    for (int i = 0; i < iterations; ++i)
        vector.append(i);

    QProgressDialog dialog;
//    dialog.setAutoClose(false);
    dialog.setLabelText(QString("Progressing using %1 thread(s)...").arg(QThread::idealThreadCount()));

        // QFutureWatcher用来监视一个QFuture,通过信号-槽机制
    QFutureWatcher<void> futureWatcher;
    QObject::connect(&futureWatcher, &QFutureWatcher<void>::finished, &dialog, &QProgressDialog::reset);
    QObject::connect(&dialog, &QProgressDialog::canceled, &futureWatcher, &QFutureWatcher<void>::cancel);
    QObject::connect(&futureWatcher,  &QFutureWatcher<void>::progressRangeChanged, &dialog, &QProgressDialog::setRange);
    QObject::connect(&futureWatcher, &QFutureWatcher<void>::progressValueChanged,  &dialog, &QProgressDialog::setValue);

    // 假装这里有复杂的计算
    std::function<void(int&)> spin = [](int &iteration) {
        const int work = 1000 * 1000 * 40;
        volatile int v = 0;
        for (int j = 0; j < work; ++j)
            ++v;

        qDebug() << "iteration" << iteration << "in thread" << QThread::currentThreadId();
    };

        // QFuture用来表征异步运算的未来的结果。
        // 可以看出,我们是先使用QFutureWatcher连接信号,再将实际的QFuture对象注入
        // map函数从线程池申请最佳的线程个数对容器中每一个项目用指定的函数并行地计算,并就地修改。线程启动之时就返回一个QFuture。
    futureWatcher.setFuture(QtConcurrent::map(vector, spin));

    // 显示进度条,并开启事件循环
    dialog.exec();

        // 让主线程不要过早的结束,等待线程完成对vector的处理后再结束。类似pthread_join()
    futureWatcher.waitForFinished();

    qDebug() << "Canceled?" << futureWatcher.future().isCanceled();
        
    std::function<QString(const QString&)> hello = [](const QString& name)
    {
        qDebug() << "Hello" << name << "from" << QThread::currentThread();
        return name;
    };
    QFuture<QString> f1 = run(hello, QString("Alice"));
    QFuture<QString> f2 = run(hello, QString("Bob"));
    f1.waitForFinished();
    f2.waitForFinished();
    qDebug() << f1.result() << f2.result();
}

你可能感兴趣的:(自学Qt系列,代码技术等,qt5,qt,多线程,线程安全)