9

在将一些 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()

谢谢你。

4

2 回答 2

5

是的,如果您需要唤醒特定线程,那么您要么需要广播唤醒,要么为该线程使用单独的条件变量。

于 2012-06-03T11:00:04.433 回答
4

不安全的举止

直接从不同的线程调用任何通用 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"));

关于线程间通信

与包含一些 QObjects 的线程进行通信的一种简单方法是通过静态QCoreApplication::postEvent()方法向其发布事件。GUI 线程是一大堆 QObject 所在的线程的常见示例。使用postEvent()始终是线程安全的。这个方法根本不关心它是从哪个线程调用的。

Qt 中散布着各种内部使用的静态方法postEvent()。它们的关键特征是它们也将是静态的!家庭就是这样的QMetaObject::invokeMethod(...)例子。

当您将信号连接到位于不同线程中的 QObject 中的槽时,将使用排队连接。通过这样的连接,每次发出信号时,QMetaCallEvent都会构造 a 并将其发布到目标 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关键是在发布另一个之前从小部件的事件队列中删除任何待处理。仅当我们确定与给定小部件的排队信号槽连接仅来自我们时,这才是安全的。这通常是一个安全的假设,因为 GUI 线程中的所有内容都使用自动连接,并且默认情况下直接调用插槽而不将事件发布到 GUI 线程的事件队列。

就是这样:

QCoreApplication::removePostedEvents(label, QEvent::MetaCall);
QMetaObject::invokeMethod(label, "setText", Q_ARG(QString, "foo"));

请注意,出于性能原因,我们使用插槽的规范化表示:没有额外的空格,并且全部const Type &转换为简单的Type.

注意:在我自己的项目中,我有时会使用私有的 Qt 事件压缩 API,但在这里我不提倡这样做。

后记

C 代码不应该调用QObject::connect,我认为这是糟糕设计的标志。如果您想从 C 代码到 Qt 代码进行通信,请使用静态QCoreApplication::postEvent(...)(当然,适当地包装/暴露给 C)。要以另一种方式进行通信,您可以在线程中手动编写事件队列。至少,“事件队列”将只是一个固定结构,但重点是使用互斥锁来保护对它的访问。

糟糕设计的另一个迹象是,GUI 类外部的代码依赖于该类本身的各个 UI 项。您应该在整个 UI 类(例如您的MainWindow)上提供一组信号和插槽,并将它们转发到内部的相关 UI 元素。您可以connect()信号到信号,但不能从槽到槽——您必须手动编码转发槽。

于 2012-06-04T14:06:53.200 回答