Qt学习笔记1.2.2 Qt Core 线程与并发编程之线程同步与事件系统


Qt Core结构

线程同步

虽然线程的目的是允许代码并行运行,但有时线程必须停止并等待其他线程。例如,如果两个线程试图同时写入同一个变量,结果是未定义的。强制线程相互等待的原理称为互斥。这是一种用于保护共享资源(如数据)的常见技术。
Qt提供了用于同步线程的低级原语和高级机制。

低级同步原语

QMutex是用于强制互斥的基本类。线程锁定互斥对象以获得对共享资源的访问权限。如果第二个线程试图在互斥锁已经锁定的情况下锁定它,那么第二个进程将进入睡眠状态,直到第一个线程完成任务并解锁互斥锁。

QReadWriteLock类似于QMutex,只是它区分了“读”和“写”访问。当一段数据没有被写入时,多个线程同时从中读取是安全的。QMutex强制多个读取器轮流读取共享数据,但QReadWriteLock允许同时读取,从而提高了并行性。

QSemaphore是QMutex的一个推广,它保护一定数量的相同资源。相反,QMutex只保护一个资源。信号量示例展示了信号量的一个典型应用:同步生产者和消费者之间对循环缓冲区的访问。

QWaitCondition不是通过强制互斥而是通过提供条件变量来同步线程。其他原语将使线程等待直到资源被解锁而QWaitCondition使线程等待直到满足特定的条件。要允许等待的线程继续,请调用wakeOne()来唤醒一个随机选择的线程,或者调用wakeAll()来同时唤醒所有线程。

注意:Qt的同步类依赖于使用正确对齐的指针。例如,不能将打包类与MSVC一起使用。

这些同步类可以用来确保方法的线程安全。然而,这样做会导致性能损失,这就是为什么大多数Qt方法都不具有线程安全性的原因。

风险

如果线程锁定了资源但没有解锁,则应用程序可能会冻结,因为其他线程将永远无法使用该资源。例如,如果抛出异常并强制当前函数返回而不释放其锁,就会发生这种情况。

另一种类似的情况是死锁。例如,假设线程A正在等待线程B解锁资源。如果线程B也在等待线程A解锁不同的资源,那么两个线程最终都将永远等待,因此应用程序将冻结。

方便类

QMutexLocker、QReadLocker和QWriteLocker是使QMutex和QReadWriteLock更容易使用的便利类。它们在构建资源时锁定资源,在销毁资源时自动解锁资源。它们旨在简化使用QMutex和QReadWriteLock的代码,从而减少资源意外被永久锁定的可能性。

高级事件队列

Qt的事件系统[对于线程间通信非常有用。每个线程都可能有自己的事件循环。要调用另一个线程中的槽(或任何invokable1方法),请将该调用放在目标线程的事件循环中。这允许目标线程在插槽开始运行之前完成其当前任务,而原始线程继续并行运行。

要在事件循环中放置调用,请进行排队的信号槽连接。无论何时发出信号,事件系统都会记录其参数。然后,信号接收器所在的线程将运行该插槽。或者,调用QMetaObject::invokeMethod()以在没有信号的情况下实现相同的效果。在这两种情况下,都必须使用排队连接,因为直接连接会绕过事件系统并立即在当前线程中运行该方法。

与使用低级原语不同,使用事件系统进行线程同步时没有死锁的风险。但是,事件系统并不强制执行互斥。如果可调用方法访问共享数据,则它们仍然必须使用低级原语进行保护。

话虽如此,Qt的事件系统以及隐式共享的数据结构提供了传统线程锁定的替代方案。如果只使用信号和槽,并且线程之间不共享任何变量,那么多线程程序可以完全不使用低级原语。

事件系统

在Qt中,事件是从抽象QEvent类派生的对象,表示应用程序内部发生的事情,或者是应用程序需要了解的外部活动的结果。事件可以由QObject子类的任何实例接收和处理,但它们与小部件特别相关。本文档描述了在典型应用程序中如何传递和处理事件。

事件是怎样传递的

当事件发生时,Qt通过构造适当的QEvent子类的实例来创建一个事件对象来表示它,并通过调用其event()函数将它传递给QObject的特定实例(或其子类之一)。
此函数不处理事件本身;根据传递的事件类型,它为特定类型的事件调用事件处理程序,并根据事件是被接受还是被忽略来发送响应。
一些事件,如QMouseEvent和QKeyEvent,来自窗口系统;一些来自其他来源,如QTimerEvent;有些来自应用程序本身。

事件类型

大多数事件类型都有特殊的类,特别是QResizeEvent、QPaintEvent、QMouseEvent、QKeyEvent和QCloseEvent。每个类子类化QEvent并添加特定于事件的函数。例如,QResizeEvent添加了size()和oldSize()使小部件能够发现它们的维度是如何更改的。

有些类支持多个实际事件类型。QMouseEvent支持鼠标按键、双击、移动和其他相关操作。

每个事件都有一个相关的类型,在QEvent::type中定义,这可以用作运行时类型信息的方便来源,以快速确定给定事件对象是从哪个子类构建的。

由于程序需要以各种复杂的方式做出反应,Qt的事件传递机制是灵活的。QCoreApplication::notify()2的文档简要地“讲述”了整个故事;《Qt季刊》的文章《Another Look at Events3》对其进行了详细的重述,对95%的应用程序进行充分的解释。

事件处理程序

传递事件的通常方式是调用虚拟函数。例如,QPaintEvent事件是通过调用QWidget::paintEvent()来传递的。这个虚拟函数负责做出适当的反应,通常是通过重新绘制小部件。如果在虚拟函数的实现中没有执行所有必要的工作,则可能需要调用基类的实现。

例如,以下代码处理自定义复选框小部件上的鼠标左键单击,同时将所有其他按钮单击传递给基本QCheckBox类:

void MyCheckBox::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        // handle left mouse button here
    } else {
        // pass on other buttons to base class
        QCheckBox::mousePressEvent(event);
    }
}

如果你想替换基类的函数,你必须自己实现所有的东西。但是,如果您只想扩展基类的功能,那么您可以实现您想要的,并调用基类以获得您不想处理的任何情况下的默认行为。

有时,没有这样的特定于事件的功能,或者特定于事件功能是不够的。最常见的例子是按Tab键。通常,QWidget会截取这些来移动键盘焦点,但一些小部件本身需要Tab键。

这些对象可以重新实现通用事件处理程序QObject::event(),并在常规处理之前或之后进行事件拦截,或者直接完全替换event()函数。一个可以拦截Tab按键事件和应用程序自定义事件的小部件可能包含以下event()函数:

bool MyWidget::event(QEvent *event)
{
    if (event->type() == QEvent::KeyPress) {
        QKeyEvent *ke = static_cast<QKeyEvent *>(event);
        if (ke->key() == Qt::Key_Tab) {
            // special tab handling here
            return true;
        }
    } else if (event->type() == MyCustomEventType) {
        MyCustomEvent *myEvent = static_cast<MyCustomEvent *>(event);
        // custom event handling here
        return true;
    }

    return QWidget::event(event);
}

注意,对于所有未处理的情况,仍然调用QWidget::event(),并且返回值指示是否处理了事件;true值可防止将事件发送到其他对象。

事件过滤器

有时,一个对象需要查看并可能拦截传递给另一个对象的事件。例如,对话框通常希望过滤某些小部件的按键;例如修改回车键的处理。

QObject::installEventFilter()函数通过设置一个事件过滤器来实现这一点,使指定的过滤器对象在其QOobject::eventFilter()功能中接收目标对象的事件。事件过滤器可以在目标对象之前处理事件,允许它根据需要检查和丢弃事件。可以使用QObject::removeEventFilter()函数删除现有的事件筛选器。

当过滤器对象的eventFilter()实现被调用时,它可以接受或拒绝事件,并允许或拒绝对事件的进一步处理。如果所有事件过滤器都允许对事件进行进一步处理(每个过滤器都返回false),则会将事件发送到目标对象本身。如果其中一个停止处理(返回true),则目标和任何后续事件过滤器根本看不到该事件。

bool FilterObject::eventFilter(QObject *object, QEvent *event)
{
    if (object == target && event->type() == QEvent::KeyPress) {
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
        if (keyEvent->key() == Qt::Key_Tab) {
            // Special tab handling
            return true;
        } else
            return false;
    }
    return false;
}

上面的代码显示了拦截发送到特定目标小部件的Tab键按下事件的另一种方法。在这种情况下,过滤器处理相关事件并返回true以阻止它们被进一步处理。所有其他事件都被忽略,过滤器返回false,允许通过安装在目标小部件上的任何其他事件过滤器将它们发送到目标小部件。

也可以通过在QApplication或QCoreApplication对象上安装事件过滤器来过滤整个应用程序的所有事件。这样的全局事件筛选器在特定于对象的筛选器之前调用。这是非常强大的,但它也降低了整个应用程序中每个单个事件的事件传递速度;通常应该使用所讨论的其他技术。

发送事件

许多应用程序都希望创建和发送自己的事件。通过构造合适的事件对象并使用QCoreApplication::sendEvent()和QCoreApplication::postEvent()发送事件,可以以与Qt自己的事件循环完全相同的方式发送事件。

sendEvent()会立即处理该事件。当它返回时,事件过滤器和/或对象本身已经处理了该事件。对于许多事件类,都有一个名为isAccepted()的函数,它告诉你事件是被调用的最后一个处理程序接受还是拒绝。

postEvent()将事件发布到队列中,以便稍后调度。下次运行Qt的主事件循环时,它会调度所有发布的事件,并进行一些优化。例如,如果存在多个调整大小事件,则会将它们压缩为一个。这同样适用于绘制事件:QWidget::update()调用postEvent(),它通过避免多次重新绘制来消除闪烁并提高速度。

postEvent()也在对象初始化期间使用,因为发布的事件通常会在对象初始化完成后很快被调度。在实现小部件时,重要的是要意识到事件可以在其生命周期的早期交付,因此,在其构造函数中,确保在收到事件之前尽早初始化成员变量。

要创建自定义类型的事件,您需要定义一个事件编号,该编号必须大于QEvent::User(1000),并且您可能需要对QEvent进行子类化,以便传递有关自定义事件的特定信息。


  1. 可调用方法,使用Q_INVOKABLE宏进行声明 ↩︎

  2. https://doc.qt.io/archives/qt-5.12/qcoreapplication.html#notify ↩︎

  3. https://doc.qt.io/archives/qq/qq11-events.html ↩︎

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值