干净地关闭Qt中的QSerialPort [英] Cleanly closing a QSerialPort in Qt

查看:348
本文介绍了干净地关闭Qt中的QSerialPort的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试关闭使用QSerialPort库打开的串行端口,但挂起时间超过一半.

I am trying to close a serial port opened using the QSerialPort library but it hangs more than half the time.

我正在开发一个多线程应用程序,其中一个线程负责UI,另一个线程负责串行通信.我正在使用QThread包装器类.

I am developing a multi-threaded app, with one thread responsible for UI and the other for serial communication. I am using the QThread wrapper class.

    void CommThread::run()
{
    serial = new QSerialPort();

    serial->setPortName(portname);
    serial->setBaudRate(QSerialPort::Baud115200);

    if(!serial->open(QIODevice::ReadWrite)){
        qDebug() << "Error opening Serial port within thread";
        quit = true;
        return;
    }else{
        /// \todo handle this exception more gracefully
    }

    /// Start our reading loop
    /// While CommThread::disconnect is not called, this loop will run
    while(!quit){
        comm_mutex->lock();

        /// If CommThread::disconnect() is called send DISCONNECT Packet
        if(dconnect){
            // Signal device to disconnect so that it can suspend USB CDC transmission of data
            qDebug() << "Entering disconnect sequence";

            serial->write(data);
            serial->flush();

            break;
        }

        /// No write or disconnect requested
        /// Read incoming data from port
        if(serial->waitForReadyRead(-1)){
            if(serial->canReadLine()){
              // Read stuff here
            }
        }

        // Transform the stuff read here

        comm_mutex->lock()
        // Do something to a shared data structure 
        // emit signal to main thread that data is ready           
        comm_mutex->unlock();
    }

    comm_mutex->unlock();

    // Thread is exiting, clean up resources it created
    qDebug() << "Thread ID" << QThread::currentThreadId();
    qDebug() << "Thread:: Closing and then deleting the serial port";
    qDebug() << "Lets check the error string" << serial->errorString();
    delete comm_mutex;
    serial->close();
    qDebug() << "Thread:: Port closed";
    delete serial;
    qDebug() << "Thread:: Serial deleted";
    delete img;
    qDebug() << "Thread:: Image deleted";
    qDebug() << "Thread:: Serial port and img memory deleted";
    quit = true;

}

问题是,当UI线程将dconnect变量设置为true并继续删除通信线程时,它被卡在通信线程的析构函数中,该析构函数如下所示:

The problem is when the UI thread sets the dconnect variable to true and proceeds to delete the communication thread it gets stuck in the destructor of the communication thread which looks like this:

    CommThread::~CommThread()
{
    qDebug() << "Destructor waiting for thread to stop";
    QThread::wait();
    qDebug() << "Destuctor Commthread ID" << QThread::currentThreadId();
    qDebug() << "Commthread wrapper exiting";
}

在三分之二的

2中,通信线程挂在serial-close()行,导致UI线程挂在析构函数中的QThread::wait()行.不用说,这将导致UI冻结,如果关闭,则整个应用程序将保留在内存中,直到被任务管理器终止.几分钟后,对serial :: close()的调用将最终返回;我想知道哪里出了问题,如何最好地避免UI死机?

2 out of three times, the communication thread hangs at the serial-close() line, causing the UI thread to hang at the QThread::wait() line in the destructor. Needless to say this results in a frozen UI and if closed, the entire application remains in memory until killed by the task manager. Given a few minutes the call to serial::close() will finally return; what I would like to know is what's wrong and how can I best avoid a hanging UI?

我已经查看了QSerialPort的代码,但看不到任何明显错误的信息.如果我调用serial->errorCode(),我会得到UknownError字符串,但是即使端口关闭而没有挂断,也会发生这种情况.

I have looked into the code of QSerialPort and I can't see anything manifestly wrong. If I call serial->errorCode() I get the UknownError string but that happens even when the port closes with no hangups.

编辑:在调试器中永远不会发生这种情况. SerialPort总是立即关闭,并且析构函数在QThread :: wait()上没有挂断的情况下顺利通过

EDIT: This NEVER happens in the debugger. The SerialPort always closes immediately and the destructor sails through with no hangups on QThread::wait()

EDIT :我肯定是serial-> close()挂起了,因为我可以看到qDebug()语句在挂起几秒钟或几分钟之前就被打印出来了.

EDIT: I am certain it is the serial->close() which is hanging because I can see the qDebug() statement being printed just before it hangs for several seconds or minutes).

设备停止传输,因为在dconnect开关中,发送了断开连接数据包,并且设备上的LED变为绿色.

The device stops transmitting because in the dconnect switch, a disconnect packet is sent and a LED on the device turns green.

推荐答案

几件事:

  1. 如果端口关闭得不够快,您当然可以简单地将其泄漏.

  1. You can certainly simply leak the port if it doesn't close soon enough.

您应该在UI响应并且尝试超时关闭线程的情况下执行正常退出.

You should perform a graceful exit where the UI is responsive and the thread shutdown is attempted with a timeout.

您应该使用智能指针和其他RAII技术来管理资源.这是C ++,而不是C.理想情况下,按值而不是通过指针存储内容.

You should use smart pointers and other RAII techniques to manage resources. This is C++, not C. Ideally, store things by value, not through a pointer.

您不得在锁定下修改共享数据结构的部分中进行阻止.

You must not block in the of the sections where you modify the shared data structure(s) under a lock.

您应该通知数据结构的更改(也许您会这样做).其他代码如何能依赖于这样的更改而无需轮询呢?不能,并且轮询对于性能而言是可怕的.

You should be notifying of changes to the data structure (perhaps you do). How can other code depend on such changes without polling otherwise? It can't, and polling is horrible for performance.

QThread为重新实现run的代码提供requestInterruptionisInterruptionRequested而没有事件循环.使用它,不要滚动获胜的quit标志.

QThread offers requestInterruption and isInterruptionRequested for code that reimplements run without an event loop. Use it, don't roll your won quit flags.

如果直接使用QObject,则您的代码会简单得多.

Your code would be much simpler if you had used a QObject directly.

至少,我们希望在关闭工作线程时不会阻塞的UI.我们从具有支持这种UI所需功能的线程实现开始.

At the very minimum, we want a UI that won't block on a worker thread being shut down. We start with a thread implementation that has the functionality needed to support such a UI.

// https://github.com/KubaO/stackoverflown/tree/master/questions/serial-test-32331713
#include <QtWidgets>

/// A thread that gives itself a bit of time to finish up, and then terminates.
class Thread : public QThread {
   Q_OBJECT
   Q_PROPERTY (int shutdownTimeout MEMBER m_shutdownTimeout)
   int m_shutdownTimeout { 1000 }; ///< in milliseconds
   QBasicTimer m_shutdownTimer;
   void timerEvent(QTimerEvent * ev) override {
      if (ev->timerId() == m_shutdownTimer.timerId()) {
         if (! isFinished()) terminate();
      }
      QThread::timerEvent(ev);
   }
   bool event(QEvent *event) override {
      if (event->type() == QEvent::ThreadChange)
         QCoreApplication::postEvent(this, new QEvent(QEvent::None));
      else if (event->type() == QEvent::None && thread() == currentThread())
         // Hint that moveToThread(this) is an antipattern
         qWarning() << "The thread controller" << this << "is running in its own thread.";
      return QThread::event(event);
   }
   using QThread::requestInterruption; ///< Hidden, use stop() instead.
   using QThread::quit; ///< Hidden, use stop() instead.
public:
   Thread(QObject * parent = 0) : QThread(parent) {
      connect(this, &QThread::finished, this, [this]{ m_shutdownTimer.stop(); });
   }
   /// Indicates that the thread is attempting to finish.
   Q_SIGNAL void stopping();
   /// Signals the thread to stop in a general way.
   Q_SLOT void stop() {
      emit stopping();
      m_shutdownTimer.start(m_shutdownTimeout, this);
      requestInterruption(); // should break a run() that has no event loop
      quit();                // should break the event loop if there is one
   }
   ~Thread() {
      Q_ASSERT(!thread() || thread() == QThread::currentThread());
      stop();
      wait(50);
      if (isRunning()) terminate();
      wait();
   }
};

Thread是-a QThread,这是一个谎言,因为我们不能在其上使用某些基类的成员,从而破坏了LSP.理想情况下,Thread应该是QObject,并且仅在内部包含QThread.

It's a bit of a lie that Thread is-a QThread since we cannot use some of the base class's members on it, thus breaking the LSP. Ideally, Thread should be a QObject, and only internally contain a QThread.

然后我们实现一个虚拟线程,该线程花费时间来终止,并且可以有选择地永久卡住,就像您的代码有时(尽管不必这样做)一样.

We then implement a dummy thread that takes its time to terminate, and can optionally get stuck permanently, just like your code sometime does (although it doesn't have to).

class LazyThread : public Thread {
   Q_OBJECT
   Q_PROPERTY(bool getStuck MEMBER m_getStuck)
   bool m_getStuck { false };
   void run() override {
      while (!isInterruptionRequested()) {
         msleep(100); // pretend that we're busy
      }
      qDebug() << "loop exited";
      if (m_getStuck) {
         qDebug() << "stuck";
         Q_FOREVER sleep(1);
      } else {
         qDebug() << "a little nap";
         sleep(2);
      }
   }
public:
   LazyThread(QObject * parent = 0) : Thread(parent) {
      setProperty("shutdownTimeout", 5000);
   }
};

然后,我们需要一个可以链接工作线程和UI关闭请求的类.它将自身作为事件过滤器安装在主窗口上,并延迟其关闭,直到所有线程终止.

We then need a class that can link up worker threads and UI close requests. It installs itself as an event filter on the main window, and delays its closing until all threads have terminated.

class CloseThreadStopper : public QObject {
   Q_OBJECT
   QSet<Thread*> m_threads;
   void done(Thread* thread ){
      m_threads.remove(thread);
      if (m_threads.isEmpty()) emit canClose();
   }
   bool eventFilter(QObject * obj, QEvent * ev) override {
      if (ev->type() == QEvent::Close) {
         bool close = true;
         for (auto thread : m_threads) {
            if (thread->isRunning() && !thread->isFinished()) {
               close = false;
               ev->ignore();
               connect(thread, &QThread::finished, this, [this, thread]{ done(thread); });
               thread->stop();
            }
         }
         return !close;
      }
      return false;
   }
public:
   Q_SIGNAL void canClose();
   CloseThreadStopper(QObject * parent = 0) : QObject(parent) {}
   void addThread(Thread* thread) {
      m_threads.insert(thread);
      connect(thread, &QObject::destroyed, this, [this, thread]{ done(thread); });
   }
   void installOn(QWidget * w) {
      w->installEventFilter(this);
      connect(this, &CloseThreadStopper::canClose, w, &QWidget::close);
   }
};

最后,我们有一个简单的UI,可让我们控制所有这些并查看其工作原理. UI绝不会无响应或被阻止.

Finally, we have a simple UI that allows us to control all this and see that it works. At no point is the UI unresponsive or blocked.

int main(int argc, char *argv[])
{
   QApplication a { argc, argv };
   LazyThread thread;
   CloseThreadStopper stopper;
   stopper.addThread(&thread);

   QWidget ui;
   QGridLayout layout { &ui };
   QLabel state;
   QPushButton start { "Start" }, stop { "Stop" };
   QCheckBox stayStuck { "Keep the thread stuck" };
   layout.addWidget(&state, 0, 0, 1, 2);
   layout.addWidget(&stayStuck, 1, 0, 1, 2);
   layout.addWidget(&start, 2, 0);
   layout.addWidget(&stop, 2, 1);
   stopper.installOn(&ui);
   QObject::connect(&stayStuck, &QCheckBox::toggled, &thread, [&thread](bool v){
      thread.setProperty("getStuck", v);
   });

   QStateMachine sm;
   QState s_started { &sm }, s_stopping { &sm }, s_stopped { &sm };
   sm.setGlobalRestorePolicy(QState::RestoreProperties);
   s_started.assignProperty(&state, "text", "Running");
   s_started.assignProperty(&start, "enabled", false);
   s_stopping.assignProperty(&state, "text", "Stopping");
   s_stopping.assignProperty(&start, "enabled", false);
   s_stopping.assignProperty(&stop, "enabled", false);
   s_stopped.assignProperty(&state, "text", "Stopped");
   s_stopped.assignProperty(&stop, "enabled", false);

   for (auto state : { &s_started, &s_stopping })
      state->addTransition(&thread, SIGNAL(finished()), &s_stopped);
   s_started.addTransition(&thread, SIGNAL(stopping()), &s_stopping);
   s_stopped.addTransition(&thread, SIGNAL(started()), &s_started);
   QObject::connect(&start, &QPushButton::clicked, [&]{ thread.start(); });
   QObject::connect(&stop, &QPushButton::clicked, &thread, &Thread::stop);
   sm.setInitialState(&s_stopped);

   sm.start();
   ui.show();
   return a.exec();
}

#include "main.moc"

给出Thread类,并按照上面的建议(第7点除外),您的run()应该大致如下:

Given the Thread class, and following advice above (other than point 7), your run() should look roughly as follows:

class CommThread : public Thread {
   Q_OBJECT
public:
   enum class Request { Disconnect };
private:
   QMutex m_mutex;
   QQueue<Request> m_requests;
   //...
   void run() override;
};

void CommThread::run()
{
   QString portname;
   QSerialPort port;

   port.setPortName(portname);
   port.setBaudRate(QSerialPort::Baud115200);

   if (!port.open(QIODevice::ReadWrite)){
      qWarning() << "Error opening Serial port within thread";
      return;
   }

   while (! isInterruptionRequested()) {
      QMutexLocker lock(&m_mutex);
      if (! m_requests.isEmpty()) {
         auto request = m_requests.dequeue();
         lock.unlock();
         if (request == Request::Disconnect) {
            qDebug() << "Entering disconnect sequence";
            QByteArray data;
            port.write(data);
            port.flush();
         }
         //...
      }
      lock.unlock();

      // The loop must run every 100ms to check for new requests
      if (port.waitForReadyRead(100)) {
         if (port.canReadLine()) {
            //...
         }
         QMutexLocker lock(&m_mutex);
         // Do something to a shared data structure
      }

      qDebug() << "The thread is exiting";
   }
}

当然,这是一种真正可怕的样式,不必要地旋转循环以等待事情发生,等等.相反,解决此类问题的简单方法是使QObject具有线程安全接口,该接口可以移至工作线程.

Of course, this is a truly horrible style that unnecessarily spins the loop waiting for things to happen, etc. Instead, the trivial way to approach such issues is to have a QObject with a thread-safe interface that can be moved to a worker thread.

首先,是一个反复出现的好帮手;有关详细信息,请参见此问题.

First, a curiously recurring helper; see this question for details.

namespace {
template <typename F>
static void postTo(QObject * obj, F && fun) {
   QObject signalSource;
   QObject::connect(&signalSource, &QObject::destroyed, obj, std::forward<F>(fun),
                    Qt::QueuedConnection);
}
}

我们从QObject派生,并使用postTo从线程的事件循环中执行函子.

We derive from QObject and use postTo to execute functors from our thread's event loop.

class CommObject : public QObject {
   Q_OBJECT
   Q_PROPERTY(QImage image READ image NOTIFY imageChanged)
   mutable QMutex m_imageMutex;
   QImage m_image;
   QByteArray m_data;
   QString m_portName;
   QSerialPort m_port { this };
   void onData() {
      if (m_port.canReadLine()) {
         // process the line
      }
      QMutexLocker lock(&m_imageMutex);
      // Do something to the image
      emit imageChanged(m_image);
   }
public:
   /// Thread-safe
   Q_SLOT void disconnect() {
      postTo(this, [this]{
         qDebug() << "Entering disconnect sequence";
         m_port.write(m_data);
         m_port.flush();
      });
   }
   /// Thread-safe
   Q_SLOT void open() {
      postTo(this, [this]{
         m_port.setPortName(m_portName);
         m_port.setBaudRate(QSerialPort::Baud115200);
         if (!m_port.open(QIODevice::ReadWrite)){
            qWarning() << "Error opening the port";
            emit openFailed();
         } else {
            emit opened();
         }
      });
   }
   Q_SIGNAL void opened();
   Q_SIGNAL void openFailed();
   Q_SIGNAL void imageChanged(const QImage &);
   CommObject(QObject * parent = 0) : QObject(parent) {
      open();
      connect(&m_port, &QIODevice::readyRead, this, &CommObject::onData);
   }
   QImage image() const {
      QMutexLocker lock(&m_imageMutex);
      return m_image;
   }
};

让我们观察一下,任何QIODevice都会在破坏时自动关闭.因此,关闭端口所需要做的就是在所需的工作线程中对其进行销毁,以便长时间操作不会阻塞UI.

Let's observe that any QIODevice automatically closes on destruction. Thus all we need to do to close the port is to destruct it, in the desired worker thread so that the long operation doesn't block the UI.

因此,我们确实希望对象(及其端口)在其线程中被删除(或泄漏).只需将Thread::stopping连接到对象的deleteLater插槽即可完成.在那里,关闭端口可能需要花费很多时间-如果Thread超时,它将终止其执行.用户界面始终保持响应状态.

Thus, we really want the object (and its port) to be deleted in its thread (or leak). This is simply accomplished by connecting Thread::stopping to the object's deleteLater slot. There, the port closing can take as much time as needed - the Thread will terminate its execution if it times out. All the while the UI remains responsive.

int main(...) {
  //...
  Thread thread;
  thread.start();
  QScopedPointer<CommObject> comm(new CommObject);
  comm->moveToThread(&thread);
  QObject::connect(&thread, &Thread::stopping, comm.take(), &QObject::deleteLater);
  //...
}

这篇关于干净地关闭Qt中的QSerialPort的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆