Qt信号参数线程安全性

3
假设我有一个信号sendImage(const QImage&),它连接到另一个线程中的一个槽updateLabel(const QImage&),该槽将把QImage转换为QPixmap,然后将其放置在QLabel中。现在我想知道,如果我将函数const QImage& prepareImage() 作为信号的参数,例如emit sendImage(prepareImage()),并且该信号每秒发射数十次,那么它是线程安全的吗?还是存在一种可能性,即prepareImage和updateLabel同时访问图像而导致程序崩溃的竞争条件?
3个回答

6
幸运的是,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仍然保持相同?如果我使用的是简单的QImage而不是const QImage&,那么它会执行深度复制吗?由于我需要修改源图像,那么使用const引用和不使用有什么区别,就性能而言呢? - user2563661
另外,显然如果在发出信号后,两个线程都开始修改由QImage的两个副本共享的数据,这将导致竞争条件,对吗? - user2563661
@user2563661 当需要时,或者当您明确调用detach时,QImage才会执行深层复制。您无法通过复制QImage实例来引发深度复制。 - Kuba hasn't forgotten Monica
@user2563661 隐式共享的整个意义在于它应该是安全的。当两个线程开始修改数据时,一个线程将获胜,阻塞另一个线程,并执行复制操作。每个 QImage 操作都可能会同步和复制图像。 - Kuba hasn't forgotten Monica
在信号上使用常量引用还是传值,有什么区别吗? - user2563661
@user2563661 是的。使用const引用将为您节省一个(浅)副本的成本。由于浅复制(至少)强制在所有核心之间进行缓存行同步作为同步手段,因此仍应避免使用。在信号的情况下,不使用const引用是过早的悲观主义。您正在浪费性能,而没有任何理由。如果您传递的是不隐式共享的类,例如std::容器,则将强制执行深度复制,这是非常浪费的,没有任何理由。 - Kuba hasn't forgotten Monica

2

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

如果您使用默认的连接方式,即Qt::AutoConnection,则对于跨线程连接,它会像Qt::QueuedConnection一样运作。

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

因此,不存在竞争条件发生的可能性。

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


1
只要您在不同的QImage实例上操作,Qt的隐式共享就是线程安全的。由于排队的槽调用接收图像的副本 - 即一个单独的实例 - 所以一切都是安全的。这是Qt防止人们犯非常愚蠢错误的情况之一。 - Kuba hasn't forgotten Monica

0

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

准备图像和更新标签两者同时访问图像时是否存在竞争条件的可能性

一个线程创建了一个对象(假设这是同一个对象),另一个线程正在使用它。在哪个时刻两个线程都在同时使用它?

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


好的,显然我没有表达清楚。这个图像是我的线程类的成员变量,只在程序启动时创建一次。另一方面,在prepareImage()中不断处理QImage中的数据。正如你所看到的,信号发出了一个图像的const引用,因此实际上两个线程应该访问同一个图像。 - user2563661
1
@user2563661 感谢Qt会保护你不让你自己搞砸,它会复制图像以免你自己踩到自己的脚。 - Kuba hasn't forgotten Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接