在Qt

时间:2015-09-01 12:31:02

标签: windows multithreading qt qt5.4 qtserialport

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

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

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

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

三次中有两次,通信线程挂起serial-close()行,导致UI线程挂在析构函数的QThread::wait()行。毋庸置疑,这会导致冻结的UI,如果关闭,整个应用程序将保留在内存中,直到被任务管理器杀死。给定几分钟后,对serial :: close()的调用将最终返回;我想知道的是什么是错的,我怎样才能最好地避免悬挂UI?

我查看了QSerialPort的代码,我看不出任何明显的错误。如果我调用serial->errorCode(),我会收到UknownError字符串,但即使端口关闭但没有挂起也会发生这种情况。

编辑:这绝不会发生在调试器中。 SerialPort总是立即关闭,析构函数在QThread :: wait()

上没有挂起的情况下完成

编辑:我确定它是serial-&gt; close(),因为我可以看到qDebug()语句在挂起几秒钟或几分钟之前就被打印出来了。

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

1 个答案:

答案 0 :(得分:4)

有几件事:

  1. 如果端口不能很快关闭,你当然可以简单地泄漏端口。

  2. 您应该在UI响应时执行正常退出,并在超时时尝试线程关闭。

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

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

  5. 您应该通知数据结构的更改(也许您可以)。其他代码如何在没有轮询的情况下依赖于此类更改?它不能,而民意调查也很糟糕。

  6. QThread提供requestInterruptionisInterruptionRequested代码,用于在没有事件循环的情况下重新实现run的代码。使用它,不要滚动你赢得的quit标志。

  7. 如果直接使用QObject,您的代码会更简单。

  8. 至少,我们想要一个不会阻止工作线程关闭的UI。我们从一个具有支持这种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是一个QThread,这是一个谎言,因为我们不能在其上使用某些基类的成员,从而打破了LSP。理想情况下,Thread应该是QObject,并且内部只包含QThread

    然后我们实现一个虚拟线程,它花费时间终止,并且可以选择永久卡住,就像你的代码有时一样(尽管它没有)。

    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关闭请求的类。它将自身安装为主窗口上的事件过滤器,并延迟关闭直到所有线程都已终止。

    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没有响应或阻止。

    screenshot

    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()应大致如下:

    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

    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从我们的线程的事件循环中执行仿函数。

    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将在超时时终止执行。用户界面始终保持响应。

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