干净地关闭 Qt 中的 QSerialPort
Cleanly closing a QSerialPort in Qt
我正在尝试关闭使用 QSerialPort 库打开的串行端口,但它挂起的时间超过一半。
我正在开发一个多线程应用程序,其中一个线程负责 UI,另一个线程负责串行通信。我正在使用 QThread 包装器 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 并继续删除通信线程时,它卡在通信线程的析构函数中,如下所示:
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() 的调用最终将 return;我想知道的是出了什么问题,我怎样才能最好地避免挂起 UI?
我查看了 QSerialPort 的代码,没有发现任何明显的错误。如果我调用 serial->errorCode()
,我会得到 UknownError 字符串,但即使端口关闭且没有挂断,也会发生这种情况。
编辑:调试器中永远不会发生这种情况。 SerialPort 总是立即关闭并且析构函数在 QThread::wait()
上没有挂断地航行通过
编辑:我确定它是 serial->close() 挂起的,因为我可以看到 qDebug() 语句在它挂起几秒钟之前被打印出来或分钟)。
设备停止传输,因为在 dconnect 开关中,发送了一个断开数据包,并且设备上的 LED 变为绿色。
几件事:
如果端口没有尽快关闭,您当然可以简单地泄漏端口。
您应该执行正常退出,其中 UI 有响应并且尝试关闭线程超时。
您应该使用智能指针和其他 RAII 技术来管理资源。这是 C++,不是 C。理想情况下,按值存储事物,而不是通过指针。
您不得在锁下修改共享数据结构的部分阻塞。
您应该通知数据结构的更改(也许您这样做了)。其他代码如何在不进行轮询的情况下依赖于此类更改?它不能,轮询对性能来说很糟糕。
QThread
提供 requestInterruption
和 isInterruptionRequested
代码,无需事件循环即可重新实现 run
。使用它,不要滚动你赢得的 quit
旗帜。
如果您直接使用 QObject
,您的代码会简单得多。
至少,我们希望 UI 不会阻塞正在关闭的工作线程。我们从一个线程实现开始,它具有支持这种 UI.
所需的功能
// https://github.com/KubaO/Whosebugn/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
是个谎言,因为我们不能在其上使用某些基础 class 的成员,从而破坏了 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);
}
};
然后我们需要一个 class 可以 link 启动工作线程并 UI 关闭请求。它在主 window 上将自己安装为事件过滤器,并延迟关闭直到所有线程都终止。
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 绝不会无响应或被阻塞。
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
class,并遵循上述建议(第 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
将终止其执行。 UI 始终保持响应。
int main(...) {
//...
Thread thread;
thread.start();
QScopedPointer<CommObject> comm(new CommObject);
comm->moveToThread(&thread);
QObject::connect(&thread, &Thread::stopping, comm.take(), &QObject::deleteLater);
//...
}
我正在尝试关闭使用 QSerialPort 库打开的串行端口,但它挂起的时间超过一半。
我正在开发一个多线程应用程序,其中一个线程负责 UI,另一个线程负责串行通信。我正在使用 QThread 包装器 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 并继续删除通信线程时,它卡在通信线程的析构函数中,如下所示:
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() 的调用最终将 return;我想知道的是出了什么问题,我怎样才能最好地避免挂起 UI?
我查看了 QSerialPort 的代码,没有发现任何明显的错误。如果我调用 serial->errorCode()
,我会得到 UknownError 字符串,但即使端口关闭且没有挂断,也会发生这种情况。
编辑:调试器中永远不会发生这种情况。 SerialPort 总是立即关闭并且析构函数在 QThread::wait()
上没有挂断地航行通过编辑:我确定它是 serial->close() 挂起的,因为我可以看到 qDebug() 语句在它挂起几秒钟之前被打印出来或分钟)。
设备停止传输,因为在 dconnect 开关中,发送了一个断开数据包,并且设备上的 LED 变为绿色。
几件事:
如果端口没有尽快关闭,您当然可以简单地泄漏端口。
您应该执行正常退出,其中 UI 有响应并且尝试关闭线程超时。
您应该使用智能指针和其他 RAII 技术来管理资源。这是 C++,不是 C。理想情况下,按值存储事物,而不是通过指针。
您不得在锁下修改共享数据结构的部分阻塞。
您应该通知数据结构的更改(也许您这样做了)。其他代码如何在不进行轮询的情况下依赖于此类更改?它不能,轮询对性能来说很糟糕。
QThread
提供requestInterruption
和isInterruptionRequested
代码,无需事件循环即可重新实现run
。使用它,不要滚动你赢得的quit
旗帜。如果您直接使用
QObject
,您的代码会简单得多。
至少,我们希望 UI 不会阻塞正在关闭的工作线程。我们从一个线程实现开始,它具有支持这种 UI.
所需的功能// https://github.com/KubaO/Whosebugn/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
是个谎言,因为我们不能在其上使用某些基础 class 的成员,从而破坏了 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);
}
};
然后我们需要一个 class 可以 link 启动工作线程并 UI 关闭请求。它在主 window 上将自己安装为事件过滤器,并延迟关闭直到所有线程都终止。
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 绝不会无响应或被阻塞。
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
class,并遵循上述建议(第 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
将终止其执行。 UI 始终保持响应。
int main(...) {
//...
Thread thread;
thread.start();
QScopedPointer<CommObject> comm(new CommObject);
comm->moveToThread(&thread);
QObject::connect(&thread, &Thread::stopping, comm.take(), &QObject::deleteLater);
//...
}