有人可以向我建议以下用例的最佳实践吗:
例如,我有一个
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
类,以保护类的读写,对吗?
一般来说,是的,出于以下原因,您需要同步对共享可变状态的访问。
现代 CPU 使用多层内存缓存来提高性能,其中部分/全部是每个核心的。除非 CPU 在执行的代码中遇到所谓的“内存屏障”,否则数据不会在缓存之间同步。内存屏障要么是专用 CPU 指令,要么是导致内存缓存同步的另一条指令的副作用。
记住这一点,考虑以下情况。 GUI 线程和 Plc
线程被安排在不同的内核上运行。
Plc
线程将速度值写入相应的
Motor
类变量中。没有内存屏障,它只写入相应的核心缓存。 GUI 线程尝试在 Plc
线程遇到内存屏障之前读取变量,并仅从主内存中获取过时的值。这当然是不可取的。C++ 通过 std::atomic
标准库类公开内存屏障功能。 Qt 还具有具有类似功能的 QAtomic
。它是一个专门针对读/写单个变量的情况的类。原则上,您可以将速度变量声明为原子的,并且对它的任何访问都会在代码中插入内存屏障。
但是,类变量通常并不独立,您与其他类变量有一定的依赖关系,并且希望在所有线程中看到一致的状态。因此,您通常会使用互斥体,因为互斥体会这样做,并且还会在其操作期间插入内存屏障(它们的实现涉及原子标志)。所以,一般来说,互斥体是必要的。我说一般是因为在你的具体情况下
GUI 线程在
Plc
Plc
线程完成使用它
Qt GUI 线程事件循环需要内存屏障才能获取下一个信号激活因此,当 GUI 线程检查电机速度时,不仅
Plc
Plc::readCompleted
信号处理程序时,还存在内存障碍。因此 GUI 线程可以“看到”电机的最新状态。
这意味着对于这种特殊情况,互斥体是不必要的。我建议记录 GUI 线程在Plc
线程完成之前不应与电机交互,因为粗心的开发人员很容易打破这个假设。