22

我有一个QThread定期生成大量数据(每秒几兆字节)的数据,它需要将其传输到父(GUI)线程。

恐怕我对内部运作不太确定,QThread所以我想寻求最佳实践。

显然,最直接的数据传输方式就是emit数组。然而,这效率如何?Qt 是否知道它在哪里使用并在发送和接收时避免对其进行深度复制?

如果没有,我可以很高兴地在主线程中分配内存并给一个指向子线程的指针,它将在其中写入数据(并且只有emit关于进度的短消息)。这对我来说似乎不是最优雅的解决方案,这就是我问的原因。

如果 Qt 在发送和接收时避免将数据复制到多个缓冲区中,是否在所有系统中都能保证?我没有资源尝试在各种操作系统下对其进行基准测试。

4

2 回答 2

41

QThread的内部工作无关紧要:它们对事件循环的工作方式没有任何作用。当emita 中的信号位于QObject与插槽对象不同的线程中时,该信号将作为 a 发布QMetaCallEvent到接收线程的事件队列中。然后在接收线程中运行的事件循环将对该事件进行操作,并在连接到发出信号的插槽中执行调用。

因此,无论发生什么,您通过信号发送的任何数据最终都将作为有效负载出现在 QEvent 派生类的实例中。

问题的核心是当QMetaCallEvent到达事件循环并且容器作为参数传递到插槽中时。当然,在此过程中可以多次调用复制构造函数。下面是一些简单的代码,演示了复制构造函数和默认构造函数实际上被调用了多少次

  • 在隐式共享的写时复制容器(QVector)的数据成员的元素上,

  • 在代表容器的自定义类上。

你会惊喜的:)

由于 Qt 容器是隐式共享的写时复制,因此它们的复制构造的成本可以忽略不计:所做的只是引用计数器在构造时自动递增。例如,不会复制任何数据成员。

唉,11 之前的 C++ 显示了它丑陋的一面:如果插槽代码以任何方式修改容器,则无法以让编译器知道不再需要原始容器的方式传递对插槽的引用。因此:如果插槽接收到对容器的 const 引用,则可以保证不会制作任何副本。如果插槽接收到容器的可写副本并且您对其进行了修改,则将生成完全不必要的副本,因为不再需要调用站点上的活动实例。在 C++-11 中,您可以将右值引用作为参数传递。在函数调用中传递右值引用会结束调用者中传递的对象的生命周期。

示例代码输出:

"Started" copies: 0 assignments: 0 default instances: 0 
"Created Foo" copies: 0 assignments: 0 default instances: 100 
"Created Bar" copies: 0 assignments: 0 default instances: 100 
"Received signal w/const container" copies: 0 assignments: 0 default instances: 100 
"Received signal w/copy of the container" copies: 0 assignments: 0 default instances: 100 
"Made a copy" copies: 100 assignments: 1 default instances: 101 
"Reset" copies: 0 assignments: 0 default instances: 0 
"Received signal w/const class" copies: 2 assignments: 0 default instances: 1 
"Received signal w/copy of the class" copies: 3 assignments: 0 default instances: 1 
//main.cpp
#include <QtCore>

class Class {
    static QAtomicInt m_copies;
    static QAtomicInt m_assignments;
    static QAtomicInt m_instances;
public:
    Class() { m_instances.fetchAndAddOrdered(1); }
    Class(const Class &) { m_copies.fetchAndAddOrdered(1); }
    Class & operator=(const Class &) { m_assignments.fetchAndAddOrdered(1); return *this; }
    static void dump(const QString & s = QString()) {
        qDebug() << s << "copies:" << m_copies << "assignments:" << m_assignments << "default instances:" << m_instances;
    }
    static void reset() {
        m_copies = 0;
        m_assignments = 0;
        m_instances = 0;
    }
};

QAtomicInt Class::m_instances;
QAtomicInt Class::m_copies;
QAtomicInt Class::m_assignments;

typedef QVector<Class> Vector;

Q_DECLARE_METATYPE(Vector)

class Foo : public QObject
{
    Q_OBJECT
    Vector v;
public:
    Foo() : v(100) {}
signals:
    void containerSignal(const Vector &);
    void classSignal(const Class &);
public slots:
    void sendContainer() { emit containerSignal(v); }
    void sendClass() { emit classSignal(Class()); }
};

class Bar : public QObject
{
    Q_OBJECT
public:
    Bar() {}
signals:
    void containerDone();
    void classDone();
public slots:
    void containerSlotConst(const Vector &) {
        Class::dump("Received signal w/const container");
    }
    void containerSlot(Vector v) {
        Class::dump("Received signal w/copy of the container");
        v[99] = Class();
        Class::dump("Made a copy");
        Class::reset();
        Class::dump("Reset");
        emit containerDone();
    }
    void classSlotConst(const Class &) {
        Class::dump("Received signal w/const class");
    }
    void classSlot(Class) {
        Class::dump("Received signal w/copy of the class");
        emit classDone();
        //QThread::currentThread()->quit();
    }
};

int main(int argc, char ** argv)
{
    QCoreApplication a(argc, argv);
    qRegisterMetaType<Vector>("Vector");
    qRegisterMetaType<Class>("Class");

    Class::dump("Started");
    QThread thread;
    Foo foo;
    Bar bar;
    Class::dump("Created Foo");
    bar.moveToThread(&thread);
    Class::dump("Created Bar");
    QObject::connect(&thread, SIGNAL(started()), &foo, SLOT(sendContainer()));
    QObject::connect(&foo, SIGNAL(containerSignal(Vector)), &bar, SLOT(containerSlotConst(Vector)));
    QObject::connect(&foo, SIGNAL(containerSignal(Vector)), &bar, SLOT(containerSlot(Vector)));
    QObject::connect(&bar, SIGNAL(containerDone()), &foo, SLOT(sendClass()));
    QObject::connect(&foo, SIGNAL(classSignal(Class)), &bar, SLOT(classSlotConst(Class)));
    QObject::connect(&foo, SIGNAL(classSignal(Class)), &bar, SLOT(classSlot(Class)));
    QObject::connect(&bar, SIGNAL(classDone()), &thread, SLOT(quit()));
    QObject::connect(&thread, SIGNAL(finished()), &a, SLOT(quit()));
    thread.start();
    a.exec();
    thread.wait();
}

#include "main.moc"
于 2012-07-02T03:29:28.050 回答
6

在通信大缓冲区时,生产者线程中的 new() 缓冲区对象是“传统的”,并且在加载时,将 *buffer 排队/发送/任何 *buffer 到消费者线程并立即 new() 另一个,(进入同一个 * buffer var),用于下一次加载数据。

问题:如果您的 GUI 线程跟不上,除非您采取一些流量控制措施(例如,预先分配一个 *buffers 池并“循环”它们),否则您将获得内存失控。

我通常做的是在一个循环中预先分配一些缓冲区实例(在大型服务器中多达数千个),并将它们的实例推送到生产者-消费者“池队列”中。如果一个子线程想要将来自某个网​​络连接的数据加载到缓冲区中,它必须从池中弹出一个并加载它。然后,它可以将缓冲区排队/发送/任何缓冲区到消费者线程,并弹出另一个缓冲区以获取更多可能进入的数据。消费者线程获取缓冲区,处理数据并将“已使用”缓冲区推回池队列中重复使用。这提供了流控制:如果子线程加载缓冲区的速度比消费者线程处理它们的速度快,它会发现池为空并阻塞它,直到消费者线程返回一些已使用的缓冲区,因此限制缓冲区/内存的使用,(以及避免持续的新/处置,

我喜欢将池队列计数转储到 1 秒计时器上的 GUI 状态栏 - 这使我可以观察缓冲区的使用情况,(并快速发现是否有泄漏:)。

于 2012-07-01T20:48:06.193 回答