从另一个线程修改 QObject 数据时的最佳实践

问题描述 投票:0回答:1

有人可以向我建议以下用例的最佳实践吗:

例如,我有一个

QObject
类,其信号位于主线程中

class Motor : public QObject
{
   Q_OBJECT
   Q_PROPERTY(int speed READ speed NOTIFY speedChanged FINAL)

   friend class Plc;

public:
   explicit Motor(QObject *parent = nullptr) :
      QObject{ parent }
   {}

   int speed() const { return m_speed; }

signals:
   void speedChanged();

protected:
   void setSpeed(int speed) {
      if(m_speed == speed)
          return;
      m_speed = speed:
      emit speedChanged();
   }

private:
   int m_speed = 0;
};

setter 被定义为受保护,因为我希望主程序只能读取该值。 只有朋友班

Plc
才能写。

Plc
类正在不同的线程上运行。它读取来自 PLC 的数据并更新
Motor
类。

下面是主线程事件循环的示例

void MainWindow::MainWindow()
{
   m_plc = new Plc();
   connect(this, &MainWindow::triggerRead, m_plc, &Plc::readPlcData);
   connect(m_plc , &Plc::readCompleted, this, &MainWindow::processData);
   m_plc->setResource(&m_motor);

   m_plcThread = new QThread(this);
   m_plc->moveToThread(m_plcThread);
   connect(m_plcThread, &QThread::started, m_plc, &Plc::readPlcData);

   thread->start();
}

void processData()
{
   // Limiting the motor speed
   if(m_motor.speed() > 20)
      plc->writeMotorSpeed(20); // Write data into PLC only. Next read will update motor.speed value

   // Do other stuff

   emit triggerRead();
}

下面是

Plc
类的示例

class Plc : public QObject
{
   Plc() : QObject{ nullptr }
   {
      connect(this, &Plc::readPlcData, this, &Plc::doReadData);
   }

   void setResource(Motor *motorPtr) { m_motor = motorPtr; }

signals:
   void readPlcData();
   void readCompleted(QPrivateSingal);

private:
   void doReadData()
   {
      // Reding data from PLC

      if(m_motor) {
         m_motor->setSpeed(data[0]);
      }

      emit readCompleted(QPrivateSingal());
   }
   
private:
   Motor *m_motor = nullptr;
};

在这种情况下,

Plc
类在主线程读取
Motor
值的同时更新它是“不可能的”。 如果我将
Motor
类暴露给 QML 以显示一些
Q_PROPERTY
,则效果相同。

UI 有一些按钮,如果单击这些按钮,主线程会检查一些

Motor
的值。在这种情况下,我认为存在
Plc
类在主线程读取值时更新值的风险。是吗?

如果后者是真的,最好用一个互斥体来保护

Motor
类,以保护类的读写,对吗?

c++ qt thread-safety
1个回答
0
投票

一般来说,是的,出于以下原因,您需要同步对共享可变状态的访问。

现代 CPU 使用多层内存缓存来提高性能,其中部分/全部是每个核心的。除非 CPU 在执行的代码中遇到所谓的“内存屏障”,否则数据不会在缓存之间同步。内存屏障要么是专用 CPU 指令,要么是导致内存缓存同步的另一条指令的副作用。 记住这一点,考虑以下情况。 GUI 线程和 Plc 线程被安排在不同的内核上运行。

Plc

线程将速度值写入相应的

Motor
类变量中。没有内存屏障,它只写入相应的核心缓存。 GUI 线程尝试在
Plc
线程遇到内存屏障之前读取变量,并仅从主内存中获取过时的值。这当然是不可取的。
C++ 通过 
std::atomic
标准库类公开内存屏障功能。 Qt 还具有具有类似功能的

QAtomic

。它是一个专门针对读/写单个变量的情况的类。原则上,您可以将速度变量声明为原子的,并且对它的任何访问都会在代码中插入内存屏障。

但是,类变量通常并不独立,您与其他类变量有一定的依赖关系,并且希望在所有线程中看到一致的状态。因此,您通常会使用互斥体,因为互斥体会这样做,并且还会在其操作期间插入内存屏障(它们的实现涉及原子标志)。
所以,一般来说,互斥体是必要的。

我说一般是因为在你的具体情况下

GUI 线程在

Plc
    线程执行期间不会与共享状态交互,直到
  1. Plc

    线程完成使用它

    
    
    Qt GUI 线程事件循环需要内存屏障才能获取下一个信号激活

  2. 因此,当 GUI 线程检查电机速度时,不仅

    Plc
  3. 线程停止使用它,而且当 GUI 线程事件循环从事件队列中获取
Plc::readCompleted

信号处理程序时,还存在内存障碍。因此 GUI 线程可以“看到”电机的最新状态。

这意味着对于这种特殊情况,互斥体是不必要的。我建议记录 GUI 线程在 
Plc
线程完成之前不应与电机交互,因为粗心的开发人员很容易打破这个假设。


© www.soinside.com 2019 - 2024. All rights reserved.