3

我在许多流行的论坛上问过这个问题,但没有具体的回应。我的应用程序使用串行通信与外部系统连接,每个系统都有自己的接口协议。从系统接收的数据显示在 Qt 4.2.1 中制作的 GUI 上。

应用程序的结构是这样的

  1. 当应用程序启动时,我们有一个登录页面,其中有四个模块可供选择。这是作为 maindisplay 类实现的。这四个模块中的每一个本身都是一个单独的类。这里涉及的模块是动作类,负责收集和显示来自各种系统的数据。

  2. 用户身份验证使他/她进入操作屏幕。动作屏幕类的构造函数执行,除了普通的初始化之外,它还启动单独的系统线程,这些线程被实现为单例。

每个系统协议都实现为以下形式的单例线程:

class SensorProtocol:public QThread {    
    static SensorProtocol* s_instance;
    SensorProtocol(){}
    SensorProtocol(const SensorProtocol&);
    operator=(const SensorProtocol&);

public:
    static SensorProtocol* getInstance();
    //miscellaneous system related data to be used for
    // data acquisition and processing
};

在实现文件 *.cpp 中:

SensorProtocol* SensorProtocol::s_instance=0;
SensorProtocol* SensorProtocol::getInstance()
{
   //DOUBLE CHECKED LOCKING PATTERN I have used singletons
   // without this overrated pattern also but just fyi  
   if(!s_instance)
   {
       mutex.lock();
       if(!s_instance) 
           s_instance=new SensorProtocol();
       mutex.unlock();
   } 
}

运行函数的结构

while(!mStop)
{
  mutex.lock()
  while(!WaitCondition.wait(&mutex,5)
  {
      if(mStop)
      return;    
  }

  //code to read from port when data becomes available
  // and process it and store in variables
  mutex.unlock();
}

在动作屏幕类中,我使用 sigaction 和 saio 定义了 InputSignalHandler。这是一个函数指针,一旦数据到达任何串行端口,它就会被激活。

它是一个全局函数(我们无法更改它,因为它特定于 Linux),它仅用于比较数据到达的串行端口的文件描述符和传感器系统的 fd,如果找到匹配项 WaitCondition.wakeOne在该线程上调用,它会等待并读取和处理数据。

在动作屏幕类中,各个线程以SensorProtocol::getInstance()->start().

每个系统的协议都有一个发送数据的帧速率。基于这一事实,在操作屏幕中,我们设置了更新计时器以在协议刷新率时超时。当这些计时器超时时,调用操作屏幕的 UpdateSensorProtocol() 函数

connect(&timer, SIGNAL(timeout), this,SLOT(UpdateSensorProtocol()));

这会抓取一个传感器单例实例作为

SensorProtocol* pSingleton=SensorProtocol::getInstance();
if(pSingleton->mUpdate)
{
    //update data on action screen GUI
   pSingleton->mUpdate=false;  //NOTE : this variable is set to
                               // true in the singleton thread
                               // while one frame is processed completely
}

对于单例实例的所有用途SensorProtocol::getInstance()都使用。鉴于上述情况,无论我做什么更改,我的协议之一都会挂起。

挂起发生在使用 UpdateSensorProtocol() 显示数据时,如果我在其中注释ShowSensorData()函数,UpdateSensorProtocol()它工作正常。但否则它会挂起并且 GUI 冻结。有什么建议么!

此外,由于主线程抓取了单例的运行实例,它真的是多线程的吗,因为我们本质上是在单例本身中更改 mUpdate,尽管是从动作屏幕开始的。

我对此感到困惑。

另外,有人可以建议我现在正在做什么的替代设计。

提前致谢

4

8 回答 8

5

首先,不要让系统单例。对不同的系统使用某种上下文封装

如果您忽略此建议并且仍想创建“单例”线程,至少将QApplication::instance();其用作线程的父级并放入QThread::wait()单例析构函数,否则您的程序将在程序退出时崩溃。

if(!s_instance){
    QMutexLocker lock(&mutex);
    if(!s_instance) 
        s_instance=new SensorProtocol( QApplication::instance());
} 

但这并不能解决您的问题...
Qt 是事件驱动的,因此请尝试利用这种非常好的事件驱动架构并为每个系统线程创建一个事件循环。然后您可以创建存在于另一个线程中的“SystemProtocols”,您可以创建计时器,在线程之间发送事件,......而不使用低级同步对象。
看看 Bradley T. Hughes Treading 的博客文章,不会感到头疼

代码未编译,但应该让您知道从哪里开始......

class GuiComponent : public QWidget {
    //...
signals: 
    void start(int); // button triggerd signal
    void stop();     // button triggerd singal 

public slots:
    // don't forget to register DataPackage at the metacompiler
    // qRegisterMetaType<DataPackage>();
    void dataFromProtocol( DataPackage ){
        // update the gui the the new data 
    }
};

class ProtocolSystem : public QObject {
     //...
    int timerId;

signals:
    void dataReady(DataPackage);

public slots:
    void stop() {
       killTimer(timerId);  
    }

    void start( int interval ) {
       timerId = startTimer();  
    }

protected:
    void timerEvent(QTimerEvent * event) {
       //code to read from port when data becomes available
       // and process it and store in dataPackage
       emit dataReady(dataPackage);
    }
};

int main( int argc, char ** argv ) {

    QApplication app( argc, argv );
    // construct the system and glue them together
    ProtocolSystem protocolSystem;
    GuiComponent gui;
    gui.connect(&protocolSystem, SIGNAL(dataReady(DataPackage)), SLOT(dataFromProtocol(DataPackage)));
    protocolSystem.connect(&gui, SIGNAL(start(int)), SLOT(start(int)));
    protocolSystem.connect(&gui, SIGNAL(stop()), SLOT(stop()));
    // move communication to its thread
    QThread protocolThread;
    protocolSystem.moveToThread(&protocolThread);
    protocolThread.start(); 
    // repeat this for other systems ...
    // start the application
    gui.show();
    app.exec();
    // stop eventloop to before closing the application
    protocolThread.quit();
    protocolThread.wait();
    return 0;    
}

现在您拥有完全独立的系统,gui 和协议现在彼此不存在,甚至不知道该程序是多线程的。您可以在单线程环境中独立地对所有系统进行单元测试,然后在实际应用程序中将它们粘合在一起,如果需要,可以在不同线程之间划分它们。
这就是我将用于解决这个问题的程序架构。没有单个低级同步元素的多线程。没有竞争条件,没有锁,......

于 2009-06-23T14:55:01.123 回答
4

问题:

使用 RAII 锁定/解锁您的互斥锁。它们目前不是异常安全的。

while(!mStop)
{
  mutex.lock()

  while(!WaitCondition.wait(&mutex,5))
  {
    if(mStop)
    {   
        // PROBLEM 1: You mutex is still locked here.
        // So returning here will leave the mutex locked forever.
        return;    
    }

    // PROBLEM 2: If you leave here via an exception.
    // This will not fire, and again you will the mutex locked forever.
    mutex.unlock();

    // Problem 3: You are using the WaitCondition() incorrectly.
    // You unlock the mutex here. The next thing that happens is a call
    // WaitCondition.wait() where the mutex MUST be locked
 }
 // PROBLEM 4
 // You are using the WaitCondition() incorrectly.
 // On exit the mutex is always locked. So nwo the mutex is locked.

您的代码应如下所示:

while(!mStop)
{
  MutextLocker   lock(mutex);  // RAII lock and unlock mutex.

  while(!WaitCondition.wait(&mutex,5))
  {
    if(mStop)
    {   
        return;    
    }

    //code to read from port when data becomes available
    // and process it and store in variables
 }

通过使用 RAII,它解决了我在上面发现的所有问题。

在旁注中。

您的双重检查锁定将无法正常工作。
通过使用“Anders Karlsson”建议的静态函数变量,您可以解决问题,因为 g++ 保证静态函数变量只会被初始化一次。此外,此方法保证了单例将被正确销毁(通过析构函数)。目前,除非您通过 onexit() 做一些花哨的事情,否则您将泄漏内存。

有关更好地实现单例的详细信息,请参见此处。
C++ 单例设计模式

在这里查看为什么您的双重检查锁定不起作用。
C++ 程序员应该了解哪些常见的未定义行为?

于 2009-06-23T05:38:20.140 回答
0

我将首先使用 RAII(资源获取即初始化)来提高锁定代码的安全性。您的代码如下所示:

mutex.lock();
...logic...
mutex.unlock();

将互斥锁代码包装在一个类中,在该类中互斥锁在 ctor 中获取并在 dtor 中释放。现在您的代码如下所示:

MyMutex mutex;
...logic...

主要的改进是,如果在逻辑部分抛出任何异常,你的互斥体仍然会被释放。

另外,不要让任何异常从您的线程中泄漏出来!即使您不知道如何处理它们,也可以将它们记录在某个地方。

于 2009-06-23T04:26:05.337 回答
0

我不能完全确定问题是什么,因为我不知道ShowSensorData()函数(方法?)在做什么,但是您包含的代码存在一些多线程问题。

  1. mUpdate如果它被多个线程访问,则应由互斥锁保护。
  2. run()方法看起来会锁定互斥体,如果mStop为真,则永远不会释放它。

您应该考虑使用RAII实践来获取和释放互斥锁。我不知道您是否正在使用 Qt 互斥锁,但您应该考虑使用QMutexLocker来锁定和解锁您的互斥锁。

我会考虑更改您的SensorProtocol类以使用条件变量和标志或某种事件(不确定 Qt 在此处提供什么)来处理与对象实例关联的方法内部的更新。就像是:

/*static*/ void
SensorProtocol::updateSensorProtocol() {
    SensorProtocol *inst = SensorProtocol::getInstance();
    inst->update();
}

然后确保该update()方法在读取或写入读取器和显示之间共享的任何成员之前获取互斥锁。

更完整的方法是使用模型-视图-控制器架构分离您的 UI 显示、传感器及其链接。将解决方案重构为 MVC 架构可能会大大简化事情。更不用说它使这样的应用程序更不容易出错。查看QAbstractItemViewQAbstractItemDelegate类,了解如何实现这一点。据我记得,有一个关于在某处使用 Qt 实现 MVC 的教程……不过,我玩 Qt 已经有好几年了。

于 2009-06-23T04:28:38.037 回答
0

看看QextSerialPort

QextSerialPort 是一个跨平台的串口类。此类封装了 POSIX 和 Windows 系统上的串行端口。

QextSerialPort继承自QIODevice并使串行端口通信更顺利地与 Qt API 的其余部分集成。

此外,您可以使用消息传递方案在 I/O 和 GUI 线程之间进行通信,而不是共享内存。这通常不太容易出错。您可以使用QApplication::postEvent函数将自定义 QEvent 消息发送到 QObject,以便在 GUI 线程中使用QObject::customeEvent处理程序进行处理。它将为您处理同步并缓解您的死锁问题。

这是一个快速而肮脏的例子:

class IODataEvent : public QEvent
{
public:
    IODataEvent() : QEvent(QEvent::User) {}

    // put all of your data here
};

class IOThread : public QThread
{
public:
    IOThread(QObject * parent) : QThread(parent) {}

    void run()
    {
    for (;;) {
            // do blocking I/O and protocol parsing
            IODataEvent *event = new IODataEvent;
            // put all of your data for the GUI into the event
            qApp->postEvent(parent(), event);
            // QApplication will take ownership of the event
        }
    }
};

class GUIObject : public QObject
{
public:
    GUIObject() : QObject(), thread(new IOThread(this)) { thread->start() }

protected:
    void customEvent(QEvent *event)
    {
        if (QEvent::User == event->type) {
            IODataEvent *data = (IODataEvent *) event;
            // get data and update GUI here
            event->accept();
        } else {
            event->ignore();
        }
        // the event loop will release the IODataEvent memory automatically
    }

private:
    IOThread *thread;
};

此外,Qt 4支持跨线程排队信号和槽

于 2009-06-23T04:28:58.927 回答
0

您的 getInstance 方法也可以这样编写,以避免使用 s_instance var:

SensorProtocol& getInstance()
{
  static SensorProtocol instance;
  return instance;
}
于 2009-06-23T04:30:09.283 回答
0

双重检查锁定模式在 C++ 中被破坏。这在整个互联网上都有很好的记录。我不知道你的问题是什么,但很明显你需要在你的代码中解决这个问题。

于 2009-06-23T04:43:14.400 回答
0

有三个单独的线程用于发送、接收和显示。

每当接收到数据时引发事件并在显示线程中处理该事件。

编辑以回应评论 1

我承认我对 qt 一无所知,但从你所说的来看,你仍然可以创建你的串行端口对象,该对象反过来启动两个工作线程(通过使用 start 方法)用于输入和输出缓冲控制。

如果串口类有一个“连接到端口”的方法来获得串口的使用;一个“打开端口”方法,它启动你的工作线程并打开端口;一个用于关闭发送和接收线程的“关闭端口”方法和一个用于设置“接收到数据”事件处理程序的属性,那么您应该全部设置好。

该类不需要是单例,因为您会发现大多数操作系统在任何时候都不会允许多个进程控制串行端口,而是会在以下情况下出现异常(您需要处理)如果它已在使用中,请尝试连接。工作线程确保端口处于您的控制之下。

于 2009-06-23T06:45:11.330 回答