【问题标题】:How to Compress Slot Calls When Using Queued Connection in Qt?在 Qt 中使用排队连接时如何压缩插槽调用?
【发布时间】:2014-01-01 08:52:25
【问题描述】:

在阅读了一些关于 Qt Signal-Slot 通信的文章like this 之后,我仍然对排队连接有疑问。

如果我有一些线程一直在相互发送信号,假设一个 thread_slow 在它的事件循环中运行一个慢速方法,而另一个 thread_fast 正在运行一个发送多个信号的快速方法,而另一个线程仍在运行它的慢方法.....当来自thread_slow 的慢方法返回事件循环时,它会处理之前由thread_fast 发送的所有信号还是仅处理最后一个(所有信号都是同类型)?

如果它会处理所有信号,有没有办法让thread_slow只处理最后一个? (考虑到多线程应用程序中的“最后一个”可能是模糊的,为了简单起见,让我们考虑线程请求最后一个信号之前的最后一个信号,因此在线程查找最后一个信号时发送的新信号可能会丢失)。

(我问这个是因为我有多个线程从多个线程接收数据,我不希望它们处理旧数据,只处理发送的最后一个数据)

我已经运行了一些测试,看起来 Qt 将处理所有信号。我做了一个线程:

while(true)
{
    QThread::msleep(500);
    emit testQueue(test);
    test++;
}

另一个插槽就可以了:

void test::testQueue(int test)
{
    test.store(private_test.load() + test);
    emit testText(QString("Test Queue: ") + QString::number(private_test.load()));
}

线程将运行:

while(true)
{
    QThread::msleep(3000);
    QCoreApplication::processEvents();
    private_test.store(private_test.load() + 1000);
}

我每 500 毫秒从一个线程向另一个线程发送一个信号,另一个线程休眠 3000 毫秒(3 秒),然后唤醒并将内部变量增加 100。每次执行插槽时它都会发出带有接收到的值 + 内部变量的文本。我得到的结果是,每次调用QCoreApplication::processEvents(); 时,都会执行所有信号......(我编辑了这部分,因为我在之前的代码中发现了一个错误)

【问题讨论】:

  • 测试起来真的很简单。直接在相关槽里放一个qDebug(),看看打印了多少次……多做几次,得出结论,不信可以自己查相关代码试用。更不用说,你总是可以在“慢”线程中编写一个共享变量,“快”将从那里获取数据。
  • 检查我的答案,我带来了有关事件循环的证据
  • @Kikohs:您的链接似乎指向源代码的错误位置。我不确定我们是否需要两个相同的答案,尽管现在您在原则上与另一个显着相似。至少,我猜它不疼。 :)
  • 我刚刚对这个问题给出了 +1(即使内容是在没有参考我的回答中给出的提示的情况下进行编辑的),因为我认为公共 API 考虑拥有此功能可能是食物.也许,它会在 Qt 中被拒绝,但至少有一个讨论它的线程是件好事。
  • 哇,我得到了 LaszloPapp 的 +1 评价?!伙计,有人举办了一个愉快的新年派对!哈哈哈

标签: c++ qt qthread qtcore qt-signals


【解决方案1】:

QCoreApplication QMetaCallEvent 压缩

每个排队的槽调用最终都会将QMetaCallEvent 发送到目标对象。该事件包含发送者对象、信号 id、槽索引和打包的调用参数。在 Qt 5 上,信号 id 通常不等于 QMetaObject::signalIndex() 返回的值:它是一个计算的索引,就好像对象只有信号方法而没有其他方法一样。

目标是压缩此类调用,以便事件队列中对于给定的(发送者对象、发送者信号、接收者对象、接收者槽)元组仅存在一个唯一调用。

这是唯一合理的方法,无需更改源或目标对象,同时保持最小的开销。我的其他答案中的事件循环递归方法每个事件都有严重的堆栈开销,当 Qt 是为 64 位指针架构构建时,大约为 1kbyte。

当新事件发布到一个对象已经发布了一个或多个事件时,可以访问事件队列。在这种情况下,QCoreApplication::postEvent 调用 QCoreApplication::compressEvent。当第一个事件发布到对象时,不会调用 compressEvent。在此方法的重新实现中,可以检查发布到目标对象的QMetaCallEvent 的内容是否有对您的槽的调用,并且必须删除过时的副本。必须包含私有 Qt 标头才能获得QMetaCallEventQPostEventQPostEventList 的定义。

优点:发送者和接收者对象都不必知道任何事情。信号和槽按原样工作,包括 Qt 5 中的方法指针调用。Qt 本身使用这种压缩事件的方式。

缺点:需要包含私有 Qt 标头并强制清除 QEvent::posted 标志。

不用破解QEvent::posted 标志,要删除的事件可以在一个单独的列表中排队,并在触发零持续时间计时器时在compressEvent 调用之外删除。这会产生额外的事件列表以及每个 event deletion iterating through the posted event list 的开销。

其他方法

采用其他方式的目的是不使用 Qt 的内部结构。

L1 第一个限制是不能访问私有定义的QMetaCallEvent 的内容。可以这样处理:

  1. 可以在源对象和目标对象之间连接具有与目标相同签名的信号和槽的代理对象。

  2. 在代理对象上运行 QMetaCallEvent 允许提取调用类型、被调用槽 id 和参数。

  3. 可以代替信号槽连接,将事件显式发布到目标对象。目标对象或事件过滤器必须从事件数据中显式重新合成槽调用。

  4. 可以使用自定义 compressedConnect 实现来代替 QObject::connect。这充分暴露了信号和槽的细节。代理对象可用于在发送者对象一侧执行与 queued_activate 的压缩友好等效。

L2 第二个限制是不能完全重新实现QCoreApplication::compressEvent,因为事件列表是私有定义的。我们仍然可以访问被压缩的事件,我们仍然可以决定是否删除它,但是没有办法迭代事件列表。因此:

  1. 可以通过从notify(因此也可以从eventFilter()event() 或插槽)递归调用sendPostedEvents 来隐式访问事件队列。这不会导致死锁,因为QCoreApplication::sendPostedEvents 在通过sendEvent 传递事件时不能(也不会)持有事件循环互斥锁。可以按如下方式过滤事件:

    • 在全球范围内重新实现QCoreApplication::notify
    • 在全球范围内通过注册QInternal::EventNotifyCallback
    • 在本地通过将事件过滤器附加到对象,
    • 通过在目标类中重新实现 QObject::event() 在本地显式地进行。

    重复的事件仍会发布到事件队列中。从sendPostedEvents 内部对notify 的递归调用会消耗相当多的堆栈空间(在 64 位指针架构上预算为 1kb)。

  2. 在向对象发布新事件之前,可以通过调用QCoreApplication::removePostedEvents 来删除已经存在的事件。不幸的是,在QCoreApplication::compressEvent 中执行此操作会导致死锁,因为事件队列互斥锁已被持有。

    包含指向接收者对象的指针的自定义事件类可以在构造函数中自动调用removePostedEvents

  3. 可以重新占用现有的压缩事件,例如QEvent::Exit

    这些事件的集合是一个实现细节,可能会发生变化。除了接收者QObject 指针之外,Qt 不会区分这些事件。实现需要每个(事件类型,接收对象)元组的代理 QObject 开销。

实施

下面的代码适用于 Qt 4 和 Qt 5。对于后者,请确保将 QT += core-private 添加到您的 qmake 项目文件中,以便包含私有 Qt 标头。

其他答案中给出了不使用 Qt 内部标头的实现:

有两个事件删除代码路径,由if (true) 选择。启用的代码路径通常会保留最近的事件并且最有意义。或者,您可能希望保留最旧的事件 - 这就是禁用代码路径的作用。

#include <QApplication>
#include <QMap>
#include <QSet>
#include <QMetaMethod>
#include <QMetaObject>
#include <private/qcoreapplication_p.h>
#include <private/qthread_p.h>
#include <private/qobject_p.h>

#include <QWidget>
#include <QPushButton>
#include <QPlainTextEdit>
#include <QSpinBox>
#include <QFormLayout>

// Works on both Qt 4 and Qt 5.

//
// Common Code

/*! Keeps a list of singal indices for one or more meatobject classes.
 * The indices are signal indices as given by QMetaCallEvent.signalId.
 * On Qt 5, those do *not* match QMetaObject::methodIndex since they
 * exclude non-signal methods. */
class SignalList {
    Q_DISABLE_COPY(SignalList)
    typedef QMap<const QMetaObject *, QSet<int> > T;
    T m_data;
    /*! Returns a signal index that is can be compared to QMetaCallEvent.signalId. */
    static int signalIndex(const QMetaMethod & method) {
        Q_ASSERT(method.methodType() == QMetaMethod::Signal);
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
        int index = -1;
        const QMetaObject * mobj = method.enclosingMetaObject();
        for (int i = 0; i <= method.methodIndex(); ++i) {
            if (mobj->method(i).methodType() != QMetaMethod::Signal) continue;
            ++ index;
        }
        return index;
#else
        return method.methodIndex();
#endif
    }
public:
    SignalList() {}
    void add(const QMetaMethod & method) {
        m_data[method.enclosingMetaObject()].insert(signalIndex(method));
    }
    void remove(const QMetaMethod & method) {
        T::iterator it = m_data.find(method.enclosingMetaObject());
        if (it != m_data.end()) {
            it->remove(signalIndex(method));
            if (it->empty()) m_data.erase(it);
        }
    }
    bool contains(const QMetaObject * metaObject, int signalId) {
        T::const_iterator it = m_data.find(metaObject);
        return it != m_data.end() && it.value().contains(signalId);
    }
};

//
// Implementation Using Event Compression With Access to Private Qt Headers

struct EventHelper : private QEvent {
    static void clearPostedFlag(QEvent * ev) {
        (&static_cast<EventHelper*>(ev)->t)[1] &= ~0x8001; // Hack to clear QEvent::posted
    }
};

template <class Base> class CompressorApplication : public Base {
    SignalList m_compressedSignals;
public:
    CompressorApplication(int & argc, char ** argv) : Base(argc, argv) {}
    void addCompressedSignal(const QMetaMethod & method) { m_compressedSignals.add(method); }
    void removeCompressedSignal(const QMetaMethod & method) { m_compressedSignals.remove(method); }
protected:
    bool compressEvent(QEvent *event, QObject *receiver, QPostEventList *postedEvents) {
        if (event->type() != QEvent::MetaCall)
            return Base::compressEvent(event, receiver, postedEvents);

        QMetaCallEvent *mce = static_cast<QMetaCallEvent*>(event);
        if (! m_compressedSignals.contains(mce->sender()->metaObject(), mce->signalId())) return false;
        for (QPostEventList::iterator it = postedEvents->begin(); it != postedEvents->end(); ++it) {
            QPostEvent &cur = *it;
            if (cur.receiver != receiver || cur.event == 0 || cur.event->type() != event->type())
                continue;
            QMetaCallEvent *cur_mce = static_cast<QMetaCallEvent*>(cur.event);
            if (cur_mce->sender() != mce->sender() || cur_mce->signalId() != mce->signalId() ||
                    cur_mce->id() != mce->id())
                continue;
            if (true) {
              /* Keep The Newest Call */              
              // We can't merely qSwap the existing posted event with the new one, since QEvent
              // keeps track of whether it has been posted. Deletion of a formerly posted event
              // takes the posted event list mutex and does a useless search of the posted event
              // list upon deletion. We thus clear the QEvent::posted flag before deletion.
              EventHelper::clearPostedFlag(cur.event);
              delete cur.event;
              cur.event = event;
            } else {
              /* Keep the Oldest Call */
              delete event;
            }
            return true;
        }
        return false;
    }
};

//
// Demo GUI

class Signaller : public QObject {
    Q_OBJECT
public:
    Q_SIGNAL void emptySignal();
    Q_SIGNAL void dataSignal(int);
};

class Widget : public QWidget {
    Q_OBJECT
    QPlainTextEdit * m_edit;
    QSpinBox * m_count;
    Signaller m_signaller;
    Q_SLOT void emptySlot() {
        m_edit->appendPlainText("emptySlot invoked");
    }
    Q_SLOT void dataSlot(int n) {
        m_edit->appendPlainText(QString("dataSlot(%1) invoked").arg(n));
    }
    Q_SLOT void sendSignals() {
        m_edit->appendPlainText(QString("\nEmitting %1 signals").arg(m_count->value()));
        for (int i = 0; i < m_count->value(); ++ i) {
            emit m_signaller.emptySignal();
            emit m_signaller.dataSignal(i + 1);
        }
    }
public:
    Widget(QWidget * parent = 0) : QWidget(parent),
        m_edit(new QPlainTextEdit), m_count(new QSpinBox)
    {
        QFormLayout * l = new QFormLayout(this);
        QPushButton * invoke = new QPushButton("Invoke");
        m_edit->setReadOnly(true);
        m_count->setRange(1, 1000);
        l->addRow("Number of slot invocations", m_count);
        l->addRow(invoke);
        l->addRow(m_edit);
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
        connect(invoke, &QPushButton::clicked, this, &Widget::sendSignals);
        connect(&m_signaller, &Signaller::emptySignal, this, &Widget::emptySlot, Qt::QueuedConnection);
        connect(&m_signaller, &Signaller::dataSignal, this, &Widget::dataSlot, Qt::QueuedConnection);
#else
        connect(invoke, SIGNAL(clicked()), SLOT(sendSignals()));
        connect(&m_signaller, SIGNAL(emptySignal()), SLOT(emptySlot()), Qt::QueuedConnection);
        connect(&m_signaller, SIGNAL(dataSignal(int)), SLOT(dataSlot(int)), Qt::QueuedConnection);
#endif
    }
};

int main(int argc, char *argv[])
{
    CompressorApplication<QApplication> a(argc, argv);
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
    a.addCompressedSignal(QMetaMethod::fromSignal(&Signaller::emptySignal));
    a.addCompressedSignal(QMetaMethod::fromSignal(&Signaller::dataSignal));
#else
    a.addCompressedSignal(Signaller::staticMetaObject.method(Signaller::staticMetaObject.indexOfSignal("emptySignal()")));
    a.addCompressedSignal(Signaller::staticMetaObject.method(Signaller::staticMetaObject.indexOfSignal("dataSignal(int)")));
#endif
    Widget w;
    w.show();
    return a.exec();
}

#include "main.moc"

【讨论】:

  • 我看到这可能是一个真正的解决方案,正是我想要的方式(即静默工作,对象甚至不知道它在那里),只获得最后一个信号,但很遗憾它必须是基于 Qt 的内部工作原理...我希望他们将来可以提供此功能...
  • > 每个排队的slot调用。 信号?
  • @kyb 这是一个通用术语,真的。您也可以对信号的调用进行排队,但排队的是接收端,而不是发送端。因此排队的槽调用更合适:排队的调用与发送者没有连接,除了通过QObject::sender()机制。
  • @KubaOber,在阅读了更多文章和帖子后,我想说信号生成并将事件发送到队列,从队列接收并由相应插槽处理的事件。你是对的。
【解决方案2】:

我正在尝试将我的评论变成一个答案。我同意您的观点,即文档缺少此信息,或者至少对我来说并不清楚,显然对您来说也是。

有两种方法可以获取更多信息:

1) 试用

将 qDebug() 或 printf()/fprintf() 语句放入“慢”线程的槽中,看看它打印出什么。运行几次,得出结论。

2) 确保

您需要阅读元对象编译器的源代码。 moc 从源文件中得到这个。这是一个更复杂的调查,但这可能会导致确定性。

据我所知,每个信号发射都会发布一个相应的事件。然后,该事件将排队等待线程类中的单独线程。在这里可以找到相关的两个源代码文件:

void QCoreApplication::postEvent(QObject *receiver, QEvent *event, int priority)

class QPostEventList : public QVector

有两种权衡取舍的方法:

从数据修改器槽中排队一个繁忙的槽操作

主要优点是在繁忙操作期间不会丢失信号。但是,这本身可能会更慢,因为它可能会处理比需要更多的操作。

这个想法是为每个处理的事件重新设置数据,但真正繁忙的操作只排队执行一次。如果有更多事件,它不一定必须是第一个事件,但这是最简单的实现。

Foo::Foo(QObject *parent) : QObject(parent)
{
    ...
    connect(this, SIGNAL(dataUpdateSignal(const QByteArray&)), SLOT(dataUpdateSlot(const QByteArray&)));
    connect(this, SIGNAL(queueBusyOperationSignal()), SLOT(busyOperation()));
    ...
}

void Foo::dataUpdateSlot(const QByteArray &data)
{
    m_data = data;

    if (busyOperationQueued);
        emit queueBusyOperationSignal();
        m_busyOperationQueued = true;
    }
}

void MyClass::busyOperationSlot()
{

    // Do the busy work with m_data here

    m_busyOperationQueued = false;    
}

连接/断开

想法是在开始处理时断开槽与相应信号的连接。这将确保不会捕获新的信号发射,并在线程空闲处理下一个事件时再次将槽连接到信号。

虽然在连接和下一个处理之间,这将在线程中有一些空闲时间,但至少这是实现它的简单方法。根据此处未真正提供的更多上下文,实际上性能差异甚至可以忽略不计。

主要缺点是在繁忙操作期间会丢失信号。

Foo::Foo(QObject *parent) : QObject(parent)
{
    ...
    connect(this, SIGNAL(dataUpdateSignal(const QByteArray&)), SLOT(busyOperationSlot(const QByteArray&)));
    ...
}

void MyClass::busyOperationSlot(const QByteArray &data)
{
    disconnect(this, SIGNAL(dataUpdateSignal(const QByteArray&)), this, SLOT(dataUpdateSlot(const QByteArray&)));

    // Do the busy work with data here

    connect(this, SIGNAL(dataUpdateSignal(const QByteArray&)), SLOT(dataUpdateSlot(const QByteArray&)));
}

未来的想法

我在想是否有一个方便的 API - 例如。一个 processEvents() 类似的方法,但有一个参数只处理最后一个发布的事件 - 用于实际告诉事件系统显式处理最后一个而不是规避问题本身。它看起来确实是这样一个 API,但是,它是私有的。

也许,有人会提交功能请求以公开发布类似的内容。

/*!
\internal
Returns \c true if \a event was compressed away (possibly deleted) and should not be added to the list.
*/
bool QCoreApplication::compressEvent(QEvent *event, QObject *receiver, QPostEventList *postedEvents)

相关源码可以在here找到。

QGuiApplicationQApplication 中似乎也有一个覆盖版本。

至于完整性,也有这样的方法:

void QCoreApplication::removePostedEvents(QObject * receiver, int eventType = 0) [static]

删除使用 postEvent() 作为接收者发布的给定 eventType 的所有事件。

事件不会被调度,而是从队列中移除。您永远不需要调用此函数。如果你调用它,请注意杀死事件可能会导致接收者破坏一个或多个不变量。

如果receiver 为null,则删除所有对象的eventType 事件。如果 eventType 为 0,则为接收者删除所有事件。你不应该在 eventType 为 0 的情况下调用这个函数。如果你以这种方式调用它,请注意杀死事件可能会导致接收者破坏一个或多个不变量。

但是根据文档,这并不是您想要的。

【讨论】:

  • 但是是否可以在运行时动态断开和重新连接?得到一个包含所有连接的列表?
  • @mFeinstein:还有see my answer。没有简单的方法可以获取连接列表。
  • @KubaOber 我刚刚看到了,是的,看起来事情并不那么容易:/ 我希望 Qt 的人看到这个帖子......
  • @mFeinstein:有几个人确实看到了。如果这很重要,我自己也是一名 Qt 开发人员,尽管我贡献的金额并不大,尤其是最近。不过,我确实和其他几个人讨论过。无论如何,显然,这不是一个有人担心过的非常常见的情况。
【解决方案3】:

这是另一种方法。它不需要更改发送者或接收者对象,但需要自定义CompressorProxy 对象。这可移植到 Qt 4 和 Qt 5,并且不需要访问 Qt 的内部。

压缩器对象必须是目标对象的子对象——带有槽的对象。这样它就可以跟踪目标对象的线程。由于压缩器的信号附加到目标槽,当它们在同一个线程中时,目标槽调用没有排队连接的开销。

神奇的发生在 emitCheck 方法中:它递归地调用自己。

  1. 槽调用结束于emitCheck
  2. 更多发布的事件通过调用sendPostedEvents发送。
  3. 如果事件队列中有任何重复的槽调用,它们将再次出现在 emitCheck 中。
  4. 一旦队列中的最后一个事件被拾取,并且sendPostedEvents 不再递归,则为给定槽重置一个标志,这样它的代理信号就不会被多次发出。这就是所需的压缩行为。

对于对CompressorProxy 实例的任何给定队列槽调用集,emitCheck 将只返回一次true,对于在传递的事件列表中被多次调用的槽。

请注意,在 32 位架构上,每次递归调用的堆栈使用量在 32 位架构上约为 600 字节,在 64 位架构上是两倍。在 OS X 的调试模式下,使用 64 位构建,每次递归使用的堆栈约为 4kb。

#include <QApplication>
#include <QWidget>
#include <QPushButton>
#include <QPlainTextEdit>
#include <QSpinBox>
#include <QFormLayout>

class CompressorProxy : public QObject {
    Q_OBJECT
    bool emitCheck(bool & flag) {
        flag = true;
        QCoreApplication::sendPostedEvents(this, QEvent::MetaCall); // recurse
        bool result = flag;
        flag = false;
        return result;
    }

    bool m_slot;
    Q_SLOT void slot() {
        if (emitCheck(m_slot)) emit signal();
    }
    Q_SIGNAL void signal();

    bool m_slot_int;
    Q_SLOT void slot_int(int arg1) {
        if (emitCheck(m_slot_int)) emit signal_int(arg1);
    }
    Q_SIGNAL void signal_int(int);
public:
    // No default constructor, since the proxy must be a child of the
    // target object.
    explicit CompressorProxy(QObject * parent) : QObject(parent) {}
};

//
// Demo GUI

class Signaller : public QObject {
    Q_OBJECT
public:
    Q_SIGNAL void emptySignal();
    Q_SIGNAL void dataSignal(int);
};

class Widget : public QWidget {
    Q_OBJECT
    QPlainTextEdit * m_edit;
    QSpinBox * m_count;
    Signaller m_signaller;
    Q_SLOT void emptySlot() {
        m_edit->appendPlainText("emptySlot invoked");
    }
    Q_SLOT void dataSlot(int n) {
        m_edit->appendPlainText(QString("dataSlot(%1) invoked").arg(n));
    }
    Q_SLOT void sendSignals() {
        m_edit->appendPlainText(QString("\nEmitting %1 signals").arg(m_count->value()));
        for (int i = 0; i < m_count->value(); ++ i) {
            emit m_signaller.emptySignal();
            emit m_signaller.dataSignal(i + 1);
        }
    }
public:
    Widget(QWidget * parent = 0) : QWidget(parent),
        m_edit(new QPlainTextEdit), m_count(new QSpinBox)
    {
        QFormLayout * l = new QFormLayout(this);
        QPushButton * invoke = new QPushButton("Invoke");
        m_edit->setReadOnly(true);
        m_count->setRange(1, 1000);
        l->addRow("Number of slot invocations", m_count);
        l->addRow(invoke);
        l->addRow(m_edit);
        connect(invoke, SIGNAL(clicked()), SLOT(sendSignals()));
        m_edit->appendPlainText(QString("Qt %1").arg(qVersion()));
        CompressorProxy * proxy = new CompressorProxy(this);
        connect(&m_signaller, SIGNAL(emptySignal()), proxy, SLOT(slot()), Qt::QueuedConnection);
        connect(&m_signaller, SIGNAL(dataSignal(int)), proxy, SLOT(slot_int(int)), Qt::QueuedConnection);
        connect(proxy, SIGNAL(signal()), this, SLOT(emptySlot()));
        connect(proxy, SIGNAL(signal_int(int)), this, SLOT(dataSlot(int)));
    }
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Widget w;
    w.show();
    return a.exec();
}

#include "main.moc"

【讨论】:

    【解决方案4】:

    这是另一种可移植到 Qt 4 和 Qt 5 的方法,并且不需要访问 Qt 的内部(除了通过公共头文件可用的内容)。在 Qt 5 上,仅支持 Qt 4 样式的连接。压缩实体是(接收器对象,槽)对。这与完全访问 QMetaCallEvent 时使用的 (sender, receiver, signal, slot) 元组不同。

    它利用QObject::qt_metacall 从黑匣子QMetaCallEvent 中窥探通话的详细信息。使用递归到 sendPostedEvents,就像在我的其他 no-internals answer 中一样。

    值得注意的是,QObject::qt_metacall 的 API 至少从 Qt 4.0 开始就保持不变。

    #include <QApplication>
    #include <QWidget>
    #include <QPushButton>
    #include <QPlainTextEdit>
    #include <QSpinBox>
    #include <QFormLayout>
    #include <QSet>
    #include <QMetaMethod>
    
    // Common Code
    
    /*! Keeps a list of method indices for one or more meatobject classes. */
    class MethodList {
        Q_DISABLE_COPY(MethodList)
        typedef QMap<const QMetaObject *, QSet<int> > T;
        T m_data;
    public:
        MethodList() {}
        template <class T> void add(const char * slot) {
            add(T::staticMetaObject.method(T::staticMetaObject.indexOfSlot(slot)));
        }
        void add(const QMetaMethod & method) {
            Q_ASSERT(method.methodIndex() >= 0);
            m_data[method.enclosingMetaObject()].insert(method.methodIndex());
        }
        void remove(const QMetaMethod & method) {
            T::iterator it = m_data.find(method.enclosingMetaObject());
            if (it != m_data.end()) {
                it->remove(method.methodIndex());
                if (it->empty()) m_data.erase(it);
            }
        }
        bool contains(const QMetaObject * metaObject, int methodId) {
            T::const_iterator it = m_data.find(metaObject);
            return it != m_data.end() && it.value().contains(methodId);
        }
    };
    Q_GLOBAL_STATIC(MethodList, compressedSlots)
    
    // Compressor
    
    class Compressor : public QObject {
        enum { Idle, Armed, Valid } m_state;
        QMetaObject::Call m_call;
        int m_methodIndex;
        QSet<int> m_armed; // armed method IDs
    
        int qt_metacall(QMetaObject::Call call, int id, void ** args) {
            if (m_state != Armed) return QObject::qt_metacall(call, id, args);
            m_state = Valid;
            m_call = call;
            m_methodIndex = id;
            return 0;
        }
        bool eventFilter(QObject * target, QEvent * ev) {
            Q_ASSERT(target == parent());
            if (ev->type() == QEvent::MetaCall) {
                m_state = Armed;
                if (QT_VERSION < QT_VERSION_CHECK(5,0,0) || ! *(void**)(ev+1)) {
                    // On Qt5, we ensure null QMetaCallEvent::slotObj_ since we can't handle Qt5-style member pointer calls
                    Compressor::event(ev); // Use QObject::event() and qt_metacall to extract metacall data
                }
                if (m_state == Armed) m_state = Idle;
                // Only intercept compressed slot calls
                if (m_state != Valid || m_call != QMetaObject::InvokeMetaMethod ||
                        ! compressedSlots()->contains(target->metaObject(), m_methodIndex)) return false;
                int methodIndex = m_methodIndex;
                m_armed.insert(methodIndex);
                QCoreApplication::sendPostedEvents(target, QEvent::MetaCall); // recurse
                if (! m_armed.contains(methodIndex)) return true; // Compress the call
                m_armed.remove(methodIndex);
            }
            return false;
        }
    public:
        Compressor(QObject * parent) : QObject(parent), m_state(Idle) {
            parent->installEventFilter(this);
        }
    };
    
    //
    // Demo GUI
    
    class Signaller : public QObject {
        Q_OBJECT
    public:
        Q_SIGNAL void emptySignal();
        Q_SIGNAL void dataSignal(int);
    };
    
    class Widget : public QWidget {
        Q_OBJECT
        QPlainTextEdit * m_edit;
        QSpinBox * m_count;
        Signaller m_signaller;
        Q_SLOT void emptySlot() {
            m_edit->appendPlainText("emptySlot invoked");
        }
        Q_SLOT void dataSlot(int n) {
            m_edit->appendPlainText(QString("dataSlot(%1) invoked").arg(n));
        }
        Q_SLOT void sendSignals() {
            m_edit->appendPlainText(QString("\nEmitting %1 signals").arg(m_count->value()));
            for (int i = 0; i < m_count->value(); ++ i) {
                emit m_signaller.emptySignal();
                emit m_signaller.dataSignal(i + 1);
            }
        }
    public:
        Widget(QWidget * parent = 0) : QWidget(parent),
            m_edit(new QPlainTextEdit), m_count(new QSpinBox)
        {
            QFormLayout * l = new QFormLayout(this);
            QPushButton * invoke = new QPushButton("Invoke");
            m_edit->setReadOnly(true);
            m_count->setRange(1, 1000);
            l->addRow("Number of slot invocations", m_count);
            l->addRow(invoke);
            l->addRow(m_edit);
            connect(invoke, SIGNAL(clicked()), SLOT(sendSignals()));
            m_edit->appendPlainText(QString("Qt %1").arg(qVersion()));
            connect(&m_signaller, SIGNAL(emptySignal()), SLOT(emptySlot()), Qt::QueuedConnection);
            connect(&m_signaller, SIGNAL(dataSignal(int)), SLOT(dataSlot(int)), Qt::QueuedConnection);
        }
    };
    
    int main(int argc, char *argv[])
    {
        QApplication a(argc, argv);
        compressedSlots()->add<Widget>("emptySlot()");
        compressedSlots()->add<Widget>("dataSlot(int)");
        Widget w;
        new Compressor(&w);
        w.show();
        return a.exec();
    }
    
    #include "main.moc"
    

    【讨论】:

    • 哈哈,肉对象 :)
    • @TimAngus 该评论现已冻结。如果需要,它应该比代码更持久。
    【解决方案5】:
    thread_slow 
    

    如果您使用队列连接或 postEvent,将处理在其事件循环中发送的所有信号

    来源:

    Queued Connection 当控制返回到接收者线程的事件循环时调用该槽。该槽在接收者的线程中执行。

    QtDoc

    如果您想了解有关事件处理方式的更多详细信息,可以查看此处:

    https://qt.gitorious.org/qt/qtbase/source/631c3dbc800bb9b2e3b227c0a09523f0f7eef0b7:src/corelib/thread/qthread_p.h#L127

    如您所见,事件按优先级排序,因此如果您的所有事件具有相同的优先级,则它是先进先出的。

    这不是一件小事,这里是粗略的尝试,告诉我它是否有效。

    我的建议是基本上自己存储事件并只处理最后一个。

    thread_slow.h

    int current_val;
    bool m_isRunning;
    

    thread_slow.cpp

    void enqueue_slot( int val /*or whatever you value is*/ ) {
         // You'll enventually need a a QMutex here if your slot is not call in the thread
         m_current_val = val;
         if( !m_isRunning )
             slowRun();
    }
    
    void checkHasReceivedEventSlot() {
        if( m_current_val != -1 ) // Invalid value or a test condition
            slowRun();
    }
    
    void slowRun() {
        m_isRunning = true;
        int v = m_current_val;
        m_current_val = -1; // Invalid value
    
       // Do stuff with v
    
       // Let the queue fill itself with enqueue_slot calls
       QTimer::singleShot(kTIMEOUT, this, SLOT(checkHasReceivedEventSlot()));
    }
    

    第一次调用enqueue_slot,会开始慢跑

    编辑:

    为确保它是最后一个事件,您可以执行以下操作:

    void checkHasReceivedEventSlot() {
        // Runs enqueue_slot until no more events are in the loop
        while( m_thread->eventDispatcher()->hasPendingEvents() )
             m_thread->eventDispatcher()->processEvents(QEventLoop::AllEvents);
    
        // m_current_val should hold the last event
        if( m_current_val != -1 ) // Invalid value or a test condition
            slowRun();
    }
    

    【讨论】:

    • "将处理在其事件循环中发送的所有信号。" -> 你能否在你的答案中加入一些证据,因为如果它不是真的,其余的都是无关紧要的,所以你首先需要预先证明这一点。 :)
    • 此外,您应该在 Qt 程序中使用 .isEmpty() 而不是 .empty()。您的代码也缺少重要的上下文,例如从未使用过“v”。目前尚不清楚第一次如何调用 checkHasRecivedEventSlot() 或如何调用 slowRun() ......等等。
    • 老兄,我知道你喜欢投反对票,这不是第一次了。我说这是一种尝试。我已经修复了代码。
    • 第一个问题,Qt要求,自己测试。如果您排队处理许多事件,您实际上可以破坏事件循环堆栈。
    • “自己测试”不是证明。文档或源代码都可以作为证明。如果我试探性地声称我有不同的经历怎么办,谁是对的……在这一点上,这是你的猜测,没有支持。实际上,您甚至没有提供一个简单的示例来支持它。
    【解决方案6】:

    来自问题:“如果它会处理所有信号,有没有办法让 thread_slow 只处理最后一个?”

    如果您只想始终处理最后一个信号,并且不介意处理 少数 个额外信号,只要它不会使事情变慢,那么您可以尝试一个非常简单的方法像这样的方法,使用常规的QThread::exec() 事件循环。将这些槽方法放入QObject 子类中,然后将其移至线程:

    //slot
    void MyClass::publicReceiverSlotForQueuedSignals(QString data)
    {
        // Update data every time
        mReceivedData = data;
    
        // Allow worker method to be queued just once
        if (!mWorkerSlotInvoked) {
            mWorkerSlotInvoked = true;
            QMetaObject::invokeMethod(this, "workerSlot", Qt::QueuedConnection);
            qDebug() << "publicReceiverSlotForQueuedSignals: invoked workerSlot!"
                     << "New data:" << mReceivedData;
        } else {
            qDebug() << "publicReceiverSlotForQueuedSignals: workerSlot already invoked."
                     << "New data:" << mReceivedData;
        }
    }
    
    //slot
    void MyClass::privateWorkerSlot()
    {
        mWorkerSlotInvoked = false;
        qDebug() << "workerSlot for data:" << mReceivedData;
        QThread::msleep(3000);
        qDebug() << "workerSlot returning.";
    }
    

    publicReceiverSlotForQueuedSignals 的通过速度非常快(else 中的qDebug 可能是快速调用最耗时的部分),因此排队的信号数量并不重要。然后privateWorkerSlot 将在该线程的每个事件循环轮换中被调用一次,无论它运行得有多慢。

    此外,在这两种插槽方法(以及您可能使用它们的其他任何地方)中添加互斥体来保护mReceivedDatamWorkerSlotInvoked 也是微不足道的。然后您可以直接连接到插槽,因为invokeMethod 是线程安全的,而互斥锁也可以使处理MyClass 线程安全的私有数据成员。只需确保将 mReceivedData 的内容复制到局部变量并解锁互斥锁,然后再进行耗时的处理即可。

    注意:未经测试的代码,可能有一些错误。

    【讨论】:

    • 我想要 LAST 信号而不是 FIRST,所以这样我有最多的更新数据,而不是要处理的旧数据
    • @mFeinstein 这将给出最后一个信号......或者至少几乎是最后一个,有一个窗口,在privateWorkerSlot 被调用之后,但在它实际被调用之前,其他线程可能会排队信号。但是由于 public slot 处理得非常快,并且假设没有其他耗时的操作,所以窗口非常短。添加互斥体甚至可以解决这个问题。
    • @hyde:根据您的代码,我不明白这将如何发生。您基本上在守卫和计时器保证的第一个调用之后停止调用。此外,“准快速”对于稳健的操作是不够的。 :) 即使您添加线程同步原语,您的代码也不能确保只处理最后发布的事件,因为为此您需要处理事件调度程序 IMO。这是它唯一可靠的来源。
    • @hyde 我没有投反对票,因为我宁愿先问你,因为也许我没有理解代码...投反对票是 LaszloPapp 的爱好,不是我的:P(请参阅 Kikohs 评论)
    • @mFeinstein 没问题 :)
    【解决方案7】:

    您可以使用 DirectConnectionQueueConnection 的组合:

    1. 在您的 Worker 方面 (thread_slow):

      • 一个公共插槽,旨在由您的任务提供者 (thread_fast) 调用

        void Working::process()
        {
           if (working)
           {
             printf("Drop a task %p\n", QThread::currentThread()); 
             return;
           }
        
          working = true;        
          emit realWork();
        }
        
      • 一个处理函数(很慢):realProcess()

        void Working::realProcess()
        {
            printf("      **** Working ... %p\n",QThread::currentThread()); fflush(stdout);
        
            // Emulate a big processing ...
            usleep(3000*1000);
        
            printf("      **** Done. %p\n",QThread::currentThread());fflush(stdout);
            working = false;
            emit done();
        }
        
      • realWorkrealProcessQueueConnection

        Working::Working()
        {
            working = false;
            connect(this,SIGNAL(realWork()),this,SLOT(realProcess()),Qt::QueuedConnection);
        }
        
    2. 在您的任务提供者方面 (thread_fast)

      • startWork() 信号

        void TaskProv::orderWork()
        {
            emit startWork();
        }
        
      • 一个 DirectConnection 到工作进程槽

        QObject::connect(&taskProvider,SIGNAL(startWork()),&worker,SLOT(process()),Qt::DirectConnection);
        

    一些注意事项:

    • 函数Working::process() 将在thread_fast 中运行(即使它是一个工作成员函数),但它只是检查一个标志,因此它不会影响处理时间

    • 如果您介意潜在的额外任务丢失,您可以使用互斥锁保护 Worker 的工作标志以进行更严格的管理。

    • 这与 lpapp 的“Queue a busy slot operation from the data mutator slot”非常相似,只是连接类型需要是 Direct 和 Queue 的正确组合。

    【讨论】:

      【解决方案8】:

      作为@kuba-ober 答案的注释-我必须在调用m_compressedSignals.contains(...) 之前更新他们的compressEvent(...) 处理程序以检查mce-&gt;sender() != nullptr,否则我的代码会出现段错误。我不确定为什么会发生这种情况,但我也没有尝试压缩所有事件,只是我系统中的一些事件。

      更新后的代码看起来像

      // Added code:
      if (mce->sender() == nullptr) {
        return Base::compressEvent(event, receiver, postedEvents);
      }
      // end added code
      if (! m_compressedSignals.contains(mce->sender()->metaObject(), mce->signalId())) return false;
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-09-20
        • 1970-01-01
        • 1970-01-01
        • 2015-01-31
        相关资源
        最近更新 更多