我试图closures使用QSerialPort库打开的串行端口,但它挂了一半以上的时间。
我正在开发一个multithreading的应用程序,一个线程负责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线程将dconnectvariables设置为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()
行上。 不用说,这会导致一个冻结的用户界面,如果closures,整个应用程序将保留在内存中,直到被任务pipe理器终止。 给几分钟时间,对serial :: close()的调用最终会返回; 我想知道的是什么是错的,我怎样才能最好地避免挂起的用户界面?
我已经查看了QSerialPort的代码,我看不到任何明显错误。 如果我打电话serial->errorCode()
我得到UknownErrorstring,但即使端口closures,没有hangups发生。
编辑 :这从来没有发生在debugging器。 SerialPort总是立即closures,并且析构函数在QThread :: wait()
编辑 :我敢肯定这是串行 – >closures()挂,因为我可以看到qDebug()语句正在打印之前,它挂了几秒钟或几分钟)。
设备停止发送,因为在dconnect交换机中,发送断开连接包,设备上的LED变为绿色。
几件事情:
如果不及时关闭,你当然可以简单地泄漏端口。
你应该在UI响应的时候执行一个优雅的退出,并且在超时的情况下尝试线程关闭。
您应该使用智能指针和其他RAII技术来管理资源。 这是C ++,而不是C.理想情况下,按价值存储事物,而不是通过指针。
您不得在锁定下修改共享数据结构的部分中进行阻止。
你应该通知数据结构的变化(也许你会这样做)。 其他代码如何依赖这种更改而不进行轮询? 它不能,投票是可怕的表现。
QThread
提供requestInterruption
和isInterruptionRequested
代码重新实现没有事件循环run
。 使用它,不要滚动你的赢得quit
标志。
如果你直接使用了QObject
你的代码会简单得多。
至少,我们想要一个不会阻塞工作线程的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没有反应或阻止。
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
的线程安全接口,可以移动到工作线程。
首先,一个奇怪的重复的帮手; 看到这个问题的细节。
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
超时, Thread
将终止它的执行。 UI一直保持响应。
int main(...) { //... Thread thread; thread.start(); QScopedPointer<CommObject> comm(new CommObject); comm->moveToThread(&thread); QObject::connect(&thread, &Thread::stopping, comm.take(), &QObject::deleteLater); //... }