zoukankan      html  css  js  c++  java
  • QT中的线程与事件循环理解(1)

    1.需要使用多线程管理的例子

      一个进程可以有一个或更多线程同时运行。线程可以看做是“轻量级进程”,进程完全由操作系统管理,线程即可以由操作系统管理,也可以由应用程序管理。Qt 使用QThread 来管理线程

        QWidget *widget = new QWidget(this);
        QVBoxLayout *layout = new QVBoxLayout;
        widget->setLayout(layout);
        QLCDNumber *lcdNumber = new QLCDNumber(this);
        layout->addWidget(lcdNumber);
        QPushButton *button = new QPushButton(tr("Start"), this);
        layout->addWidget(button);
        setCentralWidget(widget);
    
        QTimer *timer = new QTimer(this);
        connect(timer, &QTimer::timeout, [=]() {
            static int sec = 0;
            lcdNumber->display(QString::number(sec++));
        });
    
        //WorkerThread *thread = new WorkerThread(this);
        connect(button, &QPushButton::clicked, [=]() {
            timer->start(1);
            for (int i = 0; i < 2000000000; i++);
            timer->stop();
        });

      我们的主界面有一个用于显示时间的 LCD 数字面板还有一个用于启动任务的按钮。程序的目的是用户点击按钮,开始一个非常耗时的运算,程序中我们以一个 2000000000 次的循环来替代这个非常耗时的工作。同时 LCD 开始显示逝去的毫秒数。毫秒数通过一个计时器QTimer进行更新。计算完成后,计时器停止。这是一个很简单的应用,也看不出有任何问题。但是当我们开始运行程序时,问题就来了:点击按钮之后,程序界面直接停止响应,直到循环结束才开始重新更新。

       对于执行需要耗时的操作,需要多线程来进行处理。这是因为 Qt 中所有界面都是在 UI 线程中(也被称为主线程,就是执行了QApplication::exec()的线程),在这个线程中执行耗时的操作(比如那个循环),就会阻塞 UI 线程,从而让界面停止响应。界面停止响应,用户体验自然不好,不过更严重的是,有些窗口管理程序会检测到你的程序已经失去响应,可能会建议用户强制停止程序,这样一来你的程序可能就此终止,任务再也无法完成。

    2.最简单有效的方法是,通过在在UI主线程中某个功能进行非常耗时的处理时,如等待一个信号,等,一种间接的方法是,在循环等待的语句中添加QT的下列语句,这样就不会处理主UI卡死的问题,但是,这种解决方法适应面窄,只能用于处理等待中结果的主循环卡死现象。如下列语句:

        connect(button, &QPushButton::clicked, [=]() {
            timer->start(1);
            for (int i = 0; i < 2000000000; i++){
                QCoreApplication::processEvents();
            };
            timer->stop();
        });

      这样主UI线程就不会卡死。但是指标不治本。还要通过线程的方法来控制。

    3. 我们增加了一个WorkerThread类。WorkerThread继承自QThread类,重写了其run()函数。我们可以认为,run()函数就是新的线程需要执行的代码。在这里就是要执行这个循环,然后发出计算完成的信号。而在按钮点击的槽函数中,使用QThread::start()函数启动一个线程(注意,这里不是run()函数)。再次运行程序,你会发现现在界面已经不会被阻塞了。另外,我们将WorkerThread::deleteLater()函数与WorkerThread::finished()信号连接起来,当线程完成时,系统可以帮我们清除线程实例。这里的finished()信号是系统发出的

    class WorkerThread : public QThread
    {
        Q_OBJECT
    public:
        WorkerThread(QObject *parent = 0)
            : QThread(parent)
        {
        }
    protected:
        void run()
        {
            for (int i = 0; i < 1000000000; i++);
            emit done();
        }
    signals:
        void done();
    };
    
    
        QTimer *timer = new QTimer(this);
        connect(timer, &QTimer::timeout, [=]() {
            static int sec = 0;
            lcdNumber->display(QString::number(sec++));
        });
    
        WorkerThread *thread = new WorkerThread(this);
        connect(thread, &WorkerThread::done, timer, &QTimer::stop);
        connect(thread, &WorkerThread::finished, thread, &WorkerThread::deleteLater);
        connect(button, &QPushButton::clicked, [=]() {
            timer->start(1);
            thread->start();
        });

    3. 线程理解

    • 可重入的(Reentrant):如果多个线程可以在同一时刻调用一个类的所有函数,并且保证每一次函数调用都引用一个唯一的数据,就称这个类是可重入的(Reentrant means that all the functions in the referenced class can be called simultaneously by multiple threads, provided that each invocation of the functions reference unique data.)。大多数 C++ 类都是可重入的。类似的,一个函数被称为可重入的,如果该函数允许多个线程在同一时刻调用,而每一次的调用都只能使用其独有的数据。全局变量就不是函数独有的数据,而是共享的。换句话说,这意味着类或者函数的使用者必须使用某种额外的机制(比如锁)来控制对对象的实例或共享数据的序列化访问
    • 线程安全(Thread-safe):如果多个线程可以在同一时刻调用一个类的所有函数,即使每一次函数调用都引用一个共享的数据,就说这个类是线程安全的(Threadsafe means that all the functions in the referenced class can be called simultaneously by multiple threads even when each invocation references shared data.)。如果多个线程可以在同一时刻访问函数的共享数据,就称这个函数是线程安全的

       进一步说,对于一个类,如果不同的实例可以被不同线程同时使用而不受影响,就说这个类是可重入的;如果这个类的所有成员函数都可以被不同线程同时调用而不受影响,即使这些调用针对同一个对象,那么我们就说这个类是线程安全的。由此可以看出,线程安全的语义要强于可重入。

      Qt 是事件驱动的。在 Qt 中,事件由一个普通对象表示(QEvent或其子类)。这是事件与信号的一个很大区别:事件总是由某一种类型的对象表示,针对某一个特殊的对象,而信号则没有这种目标对象。所有QObject的子类都可以通过覆盖QObject::event()函数来控制事件的对象。

     4. 事件与事件队列,事件分发器,事件循环

      事件可以由程序生成,也可以在程序外部生成。

    • QKeyEventQMouseEvent对象表示键盘或鼠标的交互,通常由系统的窗口管理器产生;
    • QTimerEvent事件在定时器超时时发送给一个QObject,定时器事件通常由操作系统发出;
    • QChildEvent在增加或删除子对象时发送给一个QObject,这是由 Qt 应用程序自己发出的。

       需要注意的是,与信号不同,事件并不是一产生就被分发。

      事件产生之后被加入到一个队列中(这里的队列含义同数据结构中的概念,先进先出),该队列即被称为事件队列事件分发器遍历事件队列,如果发现事件队列中有事件,那么就把这个事件发送给它的目标对象。这个循环被称作事件循环。事件循环的伪代码描述大致如下所示: 

    while (is_active)
    {
        while (!event_queue_is_empty) {
            dispatch_next_event();
        }
        wait_for_more_events();
    }

      正如前面所说的,调用QCoreApplication::exec() 函数意味着进入了主循环。我们把事件循环理解为一个无限循环,直到QCoreApplication::exit()或者QCoreApplication::quit()被调用,事件循环才真正退出。

      伪代码里面的while会遍历整个事件队列,发送从队列中找到的事件;wait_for_more_events()函数则会阻塞事件循环,直到又有新的事件产生。我们仔细考虑这段代码,在wait_for_more_events()函数所得到的新的事件都应该是由程序外部产生的因为所有内部事件都应该在事件队列中处理完毕了。因此,我们说事件循环在wait_for_more_events()函数进入休眠,并且可以被下面几种情况唤醒:

    • 窗口管理器的动作(键盘、鼠标按键按下、与窗口交互等);
    • 套接字动作(网络传来可读的数据,或者是套接字非阻塞写等);
    • 定时器;
    • 由其它线程发出的事件(我们会在后文详细解释这种情况)。

      至于为什么需要事件循环,我们可以简单列出一个清单:

    • 组件的绘制与交互QWidget::paintEvent()会在发出QPaintEvent事件时被调用。该事件可以通过内部QWidget::update()调用或者窗口管理器(例如显示一个隐藏的窗口)发出。所有交互事件(键盘、鼠标)也是类似的:这些事件都要求有一个事件循环才能发出。
    • 定时器:长话短说,它们会在select(2)或其他类似的调用超时时被发出,因此你需要允许 Qt 通过返回事件循环来实现这些调用。
    • 网络:所有低级网络类(QTcpSocketQUdpSocket以及QTcpServer等)都是异步的。当你调用read()函数时,它们仅仅返回已可用的数据;当你调用write()函数时,它们仅仅将写入列入计划列表稍后执行。只有返回事件循环的时候,真正的读写才会执行。注意,这些类也有同步函数(以waitFor开头的函数),但是它们并不推荐使用,就是因为它们会阻塞事件循环。高级的类,例如QNetworkAccessManager则根本不提供同步 API,因此必须要求事件循环。

    5, 不要阻塞事件循环

      有了事件循环,你就会想怎样阻塞它。阻塞它的理由可能有很多,例如我就想让QNetworkAccessManager同步执行。在解释为什么永远不要阻塞事件循环之前,我们要了解究竟什么是“阻塞”。假设我们有一个按钮Button,这个按钮在点击时会发出一个信号。这个信号会与一个Worker对象连接,这个Worker对象会执行很耗时的操作。当点击了按钮之后,我们观察从上到下的函数调用堆栈:

    main(int, char **)
    QApplication::exec()
    […]
    QWidget::event(QEvent *)
    Button::mousePressEvent(QMouseEvent *)
    Button::clicked()
    […]
    Worker::doWork()

      我们在main()函数开始事件循环,也就是常见的QApplication::exec()函数。窗口管理器侦测到鼠标点击后,Qt 会发现并将其转换成QMouseEvent事件,发送给组件的event()函数。这一过程是通过QApplication::notify()函数实现的。注意我们的按钮并没有覆盖event()函数,因此其父类的实现将被执行,也就是QWidget::event()函数。这个函数发现这个事件是一个鼠标点击事件,于是调用了对应的事件处理函数,就是Button::mousePressEvent()函数。我们重写了这个函数,发出Button::clicked()信号,而正是这个信号会调用Worker::doWork()槽函数

      在worker努力工作的时候,事件循环在干什么?或许你已经猜到了答案:什么都没做!事件循环发出了鼠标按下的事件,然后等着事件处理函数返回。此时,它一直是阻塞的,直到Worker::doWork()函数结束。注意,我们使用了“阻塞”一词,也就是说,所谓阻塞事件循环,意思是没有事件被派发处理,因为事件分发被阻塞在某个地方运行,不能够继续执行其他的事件分发。

      在事件就此卡住时,组件也不会更新自身(因为QPaintEvent对象还在队列中),也不会有其它什么交互发生(还是同样的原因),定时器也不会超时并且网络交互会越来越慢直到停止。也就是说,前面我们大费周折分析的各种依赖事件循环的活动都会停止。这时候,需要窗口管理器会检测到你的应用程序不再处理任何事件,于是告诉用户你的程序失去响应。这就是为什么我们需要快速地处理事件,并且尽可能快地返回事件循环。

    6, 解决阻塞事件循环的方法

      重点来了:我们不可能避免业务逻辑中的耗时操作,那么怎样做才能既可以执行那些耗时的操作,又不会阻塞事件循环呢?

    一般会有三种解决方案:

    第一,我们将任务移到另外的线程, 多线程处理,新建线程处理该耗时任务;

    第二,我们手动强制运行事件循环。想要强制运行事件循环,我们需要在耗时的任务中一遍遍地调用QCoreApplication::processEvents()函数。QCoreApplication::processEvents()函数会发出事件队列中的所有事件,并且立即返回到调用者。仔细想一下,我们在这里所做的,就是模拟了一个事件循环。

    第三:使用使用QEventLoop类重新进入新的事件循环,即进行局部事件循环处理。通过调用QEventLoop::exec()函数,我们重新进入新的事件循环,给QEventLoop::quit()槽函数发送信号则退出这个事件循环。

      局部事件循环如下:

    QEventLoop eventLoop;
    connect(netWorker, &NetWorker::finished,  &eventLoop, &QEventLoop::quit);
    QNetworkReply *reply = netWorker->get(url);
    replyMap.insert(reply, FetchWeatherInfo);
    eventLoop.exec();

      QNetworkReply没有提供阻塞式 API,并且要求有一个事件循环。我们通过一个局部的QEventLoop来达到这一目的:当网络响应完成时,这个局部的事件循环也会退出。即在局部,可以通过局部时间循环,异质处理NetWorker获取get完成内容,如果NetWorker结束,则自动关闭该局部的事件循环。

    (1)防止局部事件循环的递归调用问题

      通过“其它的入口”进入事件循环要特别小心:因为它会导致递归调用!现在我们可以看看为什么会导致递归调用了。

      函数中调用了QCoreApplication::processEvents()函数时,用户再次点击按钮,槽函数Worker::doWork()又一次被调用:而在doWork中又进入了另一个局部事件循环。

    main(int, char **)
    QApplication::exec()
    […]
    QWidget::event(QEvent *)
    Button::mousePressEvent(QMouseEvent *)
    Button::clicked()
    […]
    Worker::doWork() // <strong>第一次调用</strong>
    QCoreApplication::processEvents() // <strong>手动发出所有事件</strong>
    […]
    QWidget::event(QEvent * ) // <strong>用户又点击了一下按钮…</strong>
    Button::mousePressEvent(QMouseEvent *)
    Button::clicked() // <strong>又发出了信号…</strong>
    […]
    Worker::doWork() // <strong>递归进入了槽函数!</strong>

      这种情况也有解决的办法:即在强制执行时间循环是,可以选择性的接受事件类型,如

      我们可以在调用QCoreApplication::processEvents()函数时传入QEventLoop::ExcludeUserInputEvents参数,意思是不要再次派发用户输入事件(这些事件仍旧会保留在事件队列中)

    7. QT中的多线程

      Qt 对线程的支持可以追溯到2000年9月22日发布的 Qt 2.2。在这个版本中,Qt 引入了QThread。不过,当时对线程的支持并不是默认开启的。Qt 4.0 开始,线程成为所有平台的默认开启选项(这意味着如果不需要线程,你可以通过编译选项关闭它,不过这不是我们现在的重点)。现在版本的 Qt 引入了很多类来支持线程。

      (1)QThread是第一个类。它也是 Qt 线程类中最核心的底层类。由于 Qt 的跨平台特性,QThread要隐藏掉所有平台相关的代码。正如前面所说,要使用QThread开始一个线程,我们可以创建它的一个子类,然后覆盖其QThread::run()函数, 然后我们这样使用新建的类来开始一个新的线程:

    class Thread : public QThread
    {
    protected:
        void run()
        {
            /* 线程的相关代码 */
        }
    };
    Thread *thread = new Thread;
    thread->start(); // 使用 start() 开始新的线程

      注意,从 Qt 4.4 开始,QThread就已经不是抽象类了。QThread::run()不再是纯虚函数,而是有了一个默认的实现。这个默认实现其实是简单地调用了QThread::exec()函数,而这个函数,按照我们前面所说的,其实是开始了一个事件循环。而对于最新的QT版本,则更简单,继承Run函数后,只需要start()就可以启动线程的时间循环了。

      (2)QRunnable是我们要介绍的第二个类。这是一个轻量级的抽象类,用于开始一个另外线程的任务。这种任务是运行过后就丢弃的。由于这个类是抽象类,我们需要继承QRunnable,然后重写其纯虚函数QRunnable::run()

      要真正执行一个QRunnable对象,我们需要使用QThreadPool类。顾名思义,这个类用于管理一个线程池。通过调用QThreadPool::start(runnable)函数,我们将一个QRunnable对象放入QThreadPool的执行队列。一旦有线程可用,线程池将会选择一个QRunnable对象,然后在那个线程开始执行。所有 Qt 应用程序都有一个全局线程池,我们可以使用QThreadPool::globalInstance()获得这个全局线程池;与此同时,我们也可以自己创建私有的线程池,并进行手动管理。

      需要注意的是,QRunnable不是一个QObject,因此也就没有内建的与其它组件交互的机制。为了与其它组件进行交互,你必须自己编写低级线程原语,例如使用 mutex 守护来获取结果等。

      (3)QtConcurrent是我们要介绍的最后一个对象。这是一个高级 API,构建于QThreadPool之上,用于处理大多数通用的并行计算模式:map、reduce 以及 filter。它还提供了QtConcurrent::run()函数,用于在另外的线程运行一个函数。注意,QtConcurrent是一个命名空间而不是一个类,因此其中的所有函数都是命名空间内的全局函数。

    不同于QThreadQRunnableQtConcurrent不要求我们使用低级同步原语:所有的QtConcurrent都返回一个QFuture对象。这个对象可以用来查询当前的运算状态(也就是任务的进度),可以用来暂停/回复/取消任务,当然也可以用来获得运算结果。注意,并不是所有的QFuture对象都支持暂停或取消的操作。比如,由QtConcurrent::run()返回的QFuture对象不能取消,但是由QtConcurrent::mappedReduced()返回的是可以的。QFutureWatcher类则用来监视QFuture的进度,我们可以用信号槽与QFutureWatcher进行交互(注意,QFuture也没有继承QObject)。

      三个多线程相关的类的对比:

    endl;

    参考博客:

      http://wiki.qt.io/Threads_Events_QObjects

  • 相关阅读:
    UVA 11488 Hyper Prefix Sets (字典树)
    UVALive 3295 Counting Triangles
    POJ 2752 Seek the Name, Seek the Fame (KMP)
    UVA 11584 Partitioning by Palindromes (字符串区间dp)
    UVA 11100 The Trip, 2007 (贪心)
    JXNU暑期选拔赛
    计蒜客---N的-2进制表示
    计蒜客---线段的总长
    计蒜客---最大质因数
    JustOj 2009: P1016 (dp)
  • 原文地址:https://www.cnblogs.com/icmzn/p/7347963.html
Copyright © 2011-2022 走看看