【问题标题】:Qt signal argument thread safetyQt 信号参数线程安全
【发布时间】:2015-06-29 14:37:35
【问题描述】:

假设我有一个信号sendImage(const QImage&) 连接到另一个线程中的插槽updateLabel(const QImage&),这会将QImage 转换为QPixmap,然后将其放置在QLabel 中。现在我想知道,如果我使用函数 const QImage& prepareImage() 作为信号的参数,例如emit sendImage(prepareImage()),并且信号每秒发出数十次,它是线程安全的还是可能在 prepareImage 和 updateLabel 之间发生竞争条件,同时访问图像从而导致程序崩溃?

【问题讨论】:

    标签: c++ multithreading qt signals-slots


    【解决方案1】:

    谢天谢地,Qt 可以保护您免受自己的伤害,并且会复制图像,这样您就不会在脚下开枪了。复制将在信号发射时完成,并且从信号的实现内部完成 - 这里,当Object::source 在调用堆栈上时完成复制。

    鉴于QImage 是隐式共享的,初始副本会很便宜,但如果主线程随后修改源图像,它将强制进行深度复制。如果您的修改导致源被丢弃,那么用新图像替换源图像而不是“修改”它会更有效。

    输出:

    data is at 0x7fff5fbffbf8 in main thread QThread(0x10250a700)
    0x7fff5fbffbf8 was copied to 0x1025115d0 in thread QThread(0x10250a700)
    got 0x1025115d0 in thread QThread(0x7fff5fbffb80)
    
    #include <QCoreApplication>
    #include <QDebug>
    #include <QThread>
    
    class Copyable {
    public:
       Copyable() {}
       Copyable(const Copyable & src) {
          qDebug() << static_cast<const void*>(&src) << "was copied to"
                   << static_cast<void*>(this) << "in thread" << QThread::currentThread();
       }
    };
    Q_DECLARE_METATYPE(Copyable)
    
    class Object : public QObject {
       Q_OBJECT
    public:
       Q_SIGNAL void source(const Copyable &);
       Q_SLOT void sink(const Copyable & data) {
          qDebug() << "got" << static_cast<const void*>(&data) << "in thread"
                   << QThread::currentThread();
          // Queue a quit since we are racing with app.exec(). qApp->quit() is a no-op before
          // the app.exec() has had a chance to block.
          QMetaObject::invokeMethod(qApp, "quit", Qt::QueuedConnection);
       }
    };
    
    class Thread : public QThread { public: ~Thread() { quit(); wait(); } };
    
    int main(int argc, char *argv[])
    {
       QCoreApplication app(argc, argv);
       Copyable data;
       qDebug() << "data is at" << static_cast<void*>(&data) << "in main thread" << app.thread();
       qRegisterMetaType<Copyable>();
       Object o1, o2;
       Thread thread;
       o2.moveToThread(&thread);
       thread.start();
       o2.connect(&o1, &Object::source, &o2, &Object::sink);
       emit o1.source(data);
       return app.exec();
    }
    
    #include "main.moc"
    

    【讨论】:

    • 感谢您的解释。所以即使复制了 QImage 本身,只要我不修改 processImage() 中的数据,QImage 指向的数据在两个线程中仍然保持不变?如果我只使用 QImage 而不是const QImage&amp;,那么它会执行深层复制吗?由于我需要修改源图像,那么就性能而言,我是否使用 const 引用有什么区别?
    • 另外,显然如果在发出信号后,两个线程都开始修改两个 QImage 副本共享的数据,这会导致竞争条件,对吧?
    • @user2563661 QImage 只会在需要时执行深层复制,或者当您明确调用 detach 时。您永远无法通过复制 QImage 实例来进行深层复制。
    • @user2563661 隐式共享的全部意义在于它应该是安全的。当两个线程都开始修改数据时,一个线程将获胜,阻塞另一个线程,并执行复制。每个QImage 操作都可能会同步和复制图像。
    • 那么在信号上使用 const 引用和按值传递有什么区别吗?
    【解决方案2】:

    这取决于SIGNAL和SLOT之间的连接。

    如果您使用默认的Qt::AutoConnection,它的作用类似于Qt::QueuedConnection 用于跨线程连接。

    在队列连接中,所有信号参数都被复制到队列中并按值传递,即使您通过引用传递它们。

    因此,不可能发生竞争条件。

    注意:QImage 实现了 CopyOnWrite(隐式共享),这意味着如果您设法以某种方式更改 QImage 的内部缓冲区,您的同步将会崩溃。

    【讨论】:

    • Qt 的隐式共享是线程安全的,只要您对QImage 的单独实例进行操作。由于排队的插槽调用接收到图像的副本 - 一个单独的实例 - 一切都是安全的。这是 Qt 防止犯非常愚蠢的错误的案例。
    【解决方案3】:

    首先,我不太明白你的意思:

    prepareImage 和 updateLabel 是否有可能同时访问图像时发生竞争情况

    一个线程创建了一个对象,并且(让我们假设这是同一个对象)另一个线程正在使用它。两个线程同时使用它的具体时间在哪里?

    即使发生这种情况,在您的情况下,在第一个线程中创建的 QImage 也会被复制并传递给另一个线程,因此这不是同一个对象。

    【讨论】:

    • 好吧,我显然不够清楚。该图像是我的线程类的成员变量,仅在程序启动时创建一次。另一方面,QImage 中的数据在 prepareImage() 中不断处理。如您所见,信号发出图像的 const 引用,因此实际上两个线程都应该访问同一个图像。
    • @user2563661 值得庆幸的是,Qt 会保护您免受自己的伤害并复制图像,这样您就不会在脚下开枪。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-05-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-09-18
    相关资源
    最近更新 更多