首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >干净地关闭Qt中的QSerialPort

问干净地关闭Qt中的QSerialPort
EN

Stack Overflow用户
提问于 2015-09-01 12:31:02
回答 1查看 4.7K关注 0票数 0

我试图关闭使用QSerialPort库打开的串行端口,但它挂起的时间超过了一半。

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

代码语言:javascript
复制
    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并继续删除通信线程时,它会卡在通信线程的析构函数中,如下所示:

代码语言:javascript
复制
    CommThread::~CommThread()
{
    qDebug() << "Destructor waiting for thread to stop";
    QThread::wait();
    qDebug() << "Destuctor Commthread ID" << QThread::currentThreadId();
    qDebug() << "Commthread wrapper exiting";
}

2/3,通信线程挂在serial-close()行,导致UI线程挂在析构函数中的QThread::wait()行。不用说,这会导致一个冻结的UI,如果关闭,整个应用程序将一直保存在内存中,直到被任务管理器杀死。给出几分钟的时间,对串行::close()的调用将最终返回;我想知道的是,到底出了什么问题,如何才能最好地避免挂起的UI?

我已经查看了QSerialPort的代码,没有发现任何明显的错误。如果我调用serial->errorCode(),就会得到UknownError字符串,但即使端口在没有通道的情况下关闭也会发生这种情况。

编辑:这在调试器中从未发生过。SerialPort总是立即关闭,析构器在QThread上没有任何障碍的情况下顺利通过::wait()

编辑:我确信是串行->close()挂起的,因为我可以看到qDebug()语句在挂起几秒钟或几分钟之前正在打印)。

设备停止传输,因为在dconnect交换机中,发送一个断开的数据包,设备上的一个LED变成绿色。

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2015-09-01 16:36:22

有几件事:

  1. 如果端口没有足够快关闭,你当然可以简单地泄漏它。
  2. 在UI响应和线程关闭尝试超时的情况下,您应该执行一个优雅的退出。
  3. 您应该使用智能指针和其他RAII技术来管理资源。这是C++,而不是C。理想情况下,按值来存储东西,而不是通过指针。
  4. 不能阻塞在锁下修改共享数据结构的部分。
  5. 您应该通知数据结构的更改(可能是这样)。其他代码如何在不轮询的情况下依赖这样的更改呢?它不能,而且轮询在性能上是很糟糕的。
  6. QThread为无需事件循环而重新实现run的代码提供了requestInterruption和isInterruptionRequested。使用它,不要滚动您赢得的quit标志。
  7. 如果直接使用QObject,代码就会简单得多。

至少,我们需要一个不会阻止工作线程被关闭的UI。我们从一个线程实现开始,它具有支持这样一个UI所需的功能。

代码语言:javascript
复制
// 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是一个小谎言--一个QThread,因为我们不能在它上使用一些基类的成员,从而破坏了LSP。理想情况下,Thread应该是一个QObject,并且只在内部包含一个QThread。

然后,我们实现一个虚拟线程,它需要花费时间来终止,并且可以选择永久地被卡住,就像您的代码有时会遇到的情况一样(尽管它不必这样做)。

代码语言:javascript
复制
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关闭请求的类。它将自己安装为主窗口上的事件筛选器,并将其关闭延迟到所有线程结束为止。

代码语言:javascript
复制
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在任何时候都不会没有响应或阻塞。

​

​

代码语言:javascript
复制
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()应该大致如下所示:

代码语言:javascript
复制
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,该接口可以移动到工作线程上。

首先,一个奇怪的反复出现的助手;有关详细信息,请参阅this question。

代码语言:javascript
复制
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执行线程事件循环中的函子。

代码语言:javascript
复制
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。

因此,我们确实希望在其线程(或泄漏)中删除对象(及其端口)。这只是通过将Thread::stopping连接到对象的deleteLater插槽来实现的。在那里,端口关闭可能需要尽可能多的时间--如果超时,Thread将终止它的执行。一直以来,UI始终保持响应性。

代码语言:javascript
复制
int main(...) {
  //...
  Thread thread;
  thread.start();
  QScopedPointer<CommObject> comm(new CommObject);
  comm->moveToThread(&thread);
  QObject::connect(&thread, &Thread::stopping, comm.take(), &QObject::deleteLater);
  //...
}
票数 4
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/32331713

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档