如何在Qt中使用排队连接时压缩插槽调用?

时间:2014-01-01 08:52:26

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

在阅读了有关Qt信号槽通信的一些文章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();时,都会执行所有信号....(我编辑了这部分因为我在之前的代码中发现了一个错误)

8 个答案:

答案 0 :(得分:13)

QCoreApplication QMetaCallEvent Compression

每个排队的插槽调用都会在向目标对象发布QMetaCallEvent时结束。该事件包含发送方对象,信号ID,插槽索引和打包的调用参数。在Qt 5中,信号id通常不等于QMetaObject::signalIndex()返回的值:它是一个计算的索引,好像该对象只有信号方法而没有其他方法。

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

这是唯一理智的方法,无需更改源或目标对象,同时保持最小的开销。我的其他答案中的事件循环递归方法每个事件都有严重的堆栈开销,当为64位指针体系结构构建Qt时,大约为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

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

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

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

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

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

      包含指向receiver对象的指针的自定义事件类可以在构造函数中自动调用QEvent::Exit

    3. 现有的压缩事件,例如QObject,可以重新使用。

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

    4. 实施

      下面的代码适用于Qt 4和Qt 5.对于后者,请确保将if (true)添加到qmake项目文件中,以便包含私有Qt标头。

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

      有两个事件删除代码路径,由#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" 选择。启用的代码路径通常会保留最近的事件并且最有意义。或者,您可能希望保留最早的事件 - 这是禁用的代码路径所做的事情。

      screenshot

      {{1}}

答案 1 :(得分:4)

我正在努力将我的评论形成答案。我同意你的意见,文件缺乏这些信息,或者至少对我来说不明确,显然也适合你。

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

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的情况下调用这个函数。如果你以这种方式调用它,请注意杀死事件可能会导致接收者破坏一个或多个不变量。

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

答案 2 :(得分:4)

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

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

魔术发生在emitCheck方法中:它以递归方式调用自身。

  1. 广告位置调用最终在emitCheck
  2. 通过致电sendPostedEvents发送进一步发布的事件。
  3. 如果事件队列中有任何重复的插槽调用,它们将再次进入emitCheck
  4. 一旦队列中的最后一个事件被拾取,并且sendPostedEvents不再递归,就会为给定的时隙重置一个标志,这样它的代理信号就不会被多次发出。这是所需的压缩行为。
  5. 对于CompressorProxy实例的任何给定排队时段调用,emitCheck只会为通过发布的多次调用的插槽返回true一次事件清单。

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

    screenshot

    #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"
    

答案 3 :(得分:3)

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

利用QObject::qt_metacall从黑方框QMetaCallEvent中窥探来电的详细信息。使用递归到sendPostedEvents,就像在我的其他no-internals answer中一样。

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

screenshot

#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"

答案 4 :(得分:2)

thread_slow 
如果您使用队列连接或postEvent

将处理在其事件循环中发送的所有信号

来源:

  

排队连接当控制返回到接收者线程的事件循环时,将调用该槽。插槽在接收器的线程中执行。

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();
}

答案 5 :(得分:2)

从问题:“如果它将处理所有信号,是否有办法使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非常快(qDebug else可能是快速调用最耗时的部分,因此排队的信号数量并不重要。然后privateWorkerSlot将被调用该线程的每个事件循环旋转一次,无论它有多慢。

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

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

答案 6 :(得分:1)

您可以结合使用 DirectConnection QueueConnection

  1. 在你的工人方面(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();
      }
      
    • QueueConnection realWorkrealProcess

      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);
      
  3. 一些注意事项:

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

    • 如果您担心潜在的额外任务下降,您可以使用互斥锁保护工作人员的工作标志,以实现更严格的管理。

    • 这非常类似于lpapp&#34;从数据变更器插槽中排队繁忙的插槽操作&#34;除了连接类型需要是Direct和Queue的正确组合。

答案 7 :(得分:0)

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

更新后的代码如下

// 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;