我是否被迫使用pthread_cond_broadcast(通过pthread_cond_signal)以保证* my * thread被唤醒?
在一些QT GUI线程(一个pthread线程)与一些C代码连接的上下文中,我偶然发现了以下问题:我启动了QT Gui线程,在我的C线程恢复其路径之前,我需要确保所有的QT Gui线程中的图形对象已经被构造出来并且它们是有效的QObjects(因为C代码将在那些上调用QObject:connect()
)。
引言放弃,等待是通过pthread_cond_wait()
+条件变量+ C线程中的关联互斥量进行的:
int isReady=0; pthread_mutex_lock(&conditionReady_mutex); while(isReady==0) { pthread_cond_wait(&conditionReady_cv, &conditionReady_mutex); } pthread_mutex_unlock(&conditionReady_mutex);
另一方面,QT Gui线程构造其图形对象,然后用以下信号发出信号:
pthread_mutex_lock(&conditionReady_mutex); isReady=1; pthread_cond_broadcast(&conditionReady_cv); pthread_mutex_unlock(&conditionReady_mutex);
基本的东西,如你所见。 但问题是:在Qt Gui线程中,我一直在使用pthread_cond_broadcast()
,以确保我的C线程被唤醒,当然。 是的,在我当前的应用程序中,我只有一个C线程和一个Qt Gui线程,并且pthread_cond_signal()
应该完成唤醒C线程的工作(因为它保证唤醒至少一个线程,并且C线程是唯一的一个)。
但是,在更一般的上下文中,假设我有三个C线程,但我希望它们中的一个(或两个)被唤醒。 一个(两个)特定线程。 我该如何确保?
如果我使用pthread_cond_signal()
,那么只能唤醒第三个线程,这将完全错过这一点,因为我感兴趣的一个线程没有被唤醒。 OTOH,通过pthread_cond_broadcast()
唤醒所有线程,甚至是那些不需要的线程,这些都是过度的。
有一种方法可以告诉pthread_cond_signal()
唤醒哪个线程?
或者,我是否应该引入更多的条件变量,以便在使用pthread_cond_broadcast()
唤醒的线程组中获得更精细的粒度?
谢谢。
是的,如果您需要唤醒特定线程,则需要广播唤醒,或者为该线程使用单独的条件变量。
不安全的举止
直接从不同的线程调用任何通用QWidget方法是不安全的,因为Qt和底层C ++的行为 – 生成的代码未定义。 它可能会发动核爆击。 你被警告了。 因此,除非使用Qt提供的安全的线程间通信,否则不能从单独的线程调用QWidget::update()
, QLabel::setText(...)
或任何其他此类方法。
错误
// in a separate thread widget->update(); widget->setText("foo");
对
// in a separate thread // using QMetaObject::invokeMethod(widget, "update") is unoptimal QCoreApplication::postEvent(widget, new QEvent(QEvent::UpdateRequest), Qt::LowEventPriority); QMetaObject::invokeMethod(widget, "setText", Q_ARG(QString, "foo"));
论网络传播
与包含一些QObject的线程进行通信的一种简单方法是通过静态QCoreApplication::postEvent()
方法QCoreApplication::postEvent()
发布事件。 GUI线程是一大堆QObject所在的线程的常见示例。 使用postEvent()
始终是线程安全的。 这个方法根本不关心从哪个线程调用它。
Qt内部有各种静态方法,内部使用postEvent()
。 他们的关键签名是,他们也将是静态的! QMetaObject::invokeMethod(...)
系列就是这样一个例子。
将信号连接到生活在不同线程中的QObject中的插槽时,将使用排队连接。 有了这样的连接,每次发出信号时, QMetaCallEvent
构造一个QMetaCallEvent
并将其发布到目标QObject,使用 – 你猜对了 – QCoreApplication::postEvent()
。
因此,将来自某个非GUI线程中的QObject的信号连接到QWidget::update()
插槽是安全的,因为在内部这样的连接使用postEvent()
来跨越线程障碍传播信号。 GUI线程中的事件循环将拾取它并执行对标签上的QWidget::update()
的实际调用。 完全线程安全。
优化
在线程中使用信号槽连接或方法调用的唯一问题是Qt不知道您的更新应该被压缩。 每次在单独的线程中发出连接到QWidget::update()
信号,或者每次调用invokeMethod
,都会有一个事件发布到事件队列。
进行跨线程更新的惯用方法(如果没有记录)是:
QApplication::postEvent(widget, new QEvent(QEvent::UpdateRequest), Qt::LowEventPriority);
UpdateRequest事件由Qt压缩。 在任何时候,特定小部件的事件队列中只能有一个这样的事件。 因此,一旦您首先发布此类事件,后续的事件将被忽略,直到窗口小部件实际消耗该事件并重新绘制(更新)本身。
那么,我们的各种小部件设置调用如何,例如QLabel::setText()
? 当然,如果我们能以某种方式压缩这些也会很酷吗? 是的,这肯定是可能的,但我们必须限制自己使用公共Qt接口。 Qt在这里没有提供任何明显的东西。
关键是在发布另一个QMetaCallEvent
之前从窗口小部件的事件队列中删除任何挂起的QMetaCallEvent
。 只有当我们确定给定小部件的排队信号槽连接仅来自我们时,这才是安全的。 这通常是一个安全的假设,因为GUI线程中的所有内容都使用自动连接,并且这些默认情况下将直接调用插槽而不将事件发布到GUI线程的事件队列。
这是如何做:
QCoreApplication::removePostedEvents(label, QEvent::MetaCall); QMetaObject::invokeMethod(label, "setText", Q_ARG(QString, "foo"));
请注意,出于性能原因,我们使用了槽的规范化表示:没有多余的空格,所有的const Type &
都被转换为简单的Type
。
注意:在我自己的项目中,我有时使用私有Qt事件压缩API,但我不会在此提倡这样做。
Postscriptum
C代码不应该调用QObject::connect
,我认为这是设计糟糕的标志。 如果你想从C代码到Qt代码进行通信,请使用静态QCoreApplication::postEvent(...)
(当然,适当地包装/暴露给C)。 要以另一种方式进行通信,您可以手动编写线程中的事件队列。 “事件队列”至少只是一个固定的结构,但重点是使用互斥锁来保护对它的访问。
设计错误的另一个迹象是GUI类外部的代码依赖于该类本身的各个UI项。 您应该在整个UI类(比如您的MainWindow
)上提供一组信号和插槽,并在内部将它们转发到相关的UI元素。 您可以connect()
信号connect()
到信号,但不能将插槽连接到插槽 – 您必须手动编码转发插槽。