Чистое закрытие QSerialPort в Qt
Я пытаюсь закрыть последовательный порт, открытый с помощью библиотеки QSerialPort, но он зависает более половины времени.
Я разрабатываю многопоточное приложение, один поток которого отвечает за пользовательский интерфейс, а другой - за последовательную связь. Я использую класс-оболочку 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;
}
Проблема в том, что поток пользовательского интерфейса устанавливает для переменной 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()
линии, в результате чего поток пользовательского интерфейса зависает на QThread::wait()
линия в деструкторе. Нет необходимости говорить, что это приводит к зависанию пользовательского интерфейса и, если оно закрыто, все приложение остается в памяти, пока не будет уничтожено диспетчером задач. Через несколько минут вызов serial::close() наконец вернется; я хотел бы знать, что не так и как мне лучше избежать зависания пользовательского интерфейса?
Я изучил код QSerialPort и не вижу ничего явно неправильного. Если я позвоню serial->errorCode()
Я получаю строку UknownError, но это происходит, даже когда порт закрывается без зависаний.
РЕДАКТИРОВАТЬ: Это НИКОГДА не происходит в отладчике. SerialPort всегда закрывается немедленно, и деструктор пропускается без всяких зависаний в QThread::wait()
РЕДАКТИРОВАТЬ: я уверен, что это serial->close(), который висит, потому что я вижу, как оператор qDebug() печатается непосредственно перед тем, как он зависает на несколько секунд или минут).
Устройство прекращает передачу, поскольку в коммутаторе dconnect отправляется пакет разъединения, а светодиод на устройстве загорается зеленым.
1 ответ
Несколько вещей:
Конечно, вы можете просто утечь порт, если он не закроется достаточно скоро.
Вы должны выполнить изящный выход, когда пользовательский интерфейс реагирует и попытка завершения потока выполняется с таймаутом.
Вы должны использовать умные указатели и другие методы RAII для управления ресурсами. Это C++, а не C. В идеале, хранить вещи по значению, а не через указатель.
Вы не должны блокировать разделы, в которых вы изменяете общие структуры данных под блокировкой.
Вы должны уведомлять об изменениях в структуре данных (возможно, вы делаете). Как другой код может зависеть от таких изменений, не опрашивая иначе? Это не может, и опрос ужасен для производительности.
QThread
предложенияrequestInterruption
а такжеisInterruptionRequested
для кода, который переопределяетrun
без цикла событий. Используйте это, не бросайте свой выигранныйquit
флаги.Ваш код был бы намного проще, если бы вы использовали
QObject
непосредственно.
Как минимум, мы хотим, чтобы пользовательский интерфейс не блокировал закрытие рабочего потока. Мы начнем с реализации потоков, которая обладает функциональностью, необходимой для поддержки такого интерфейса.
// https://github.com/KubaO/stackrun/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);
}
};
Затем нам нужен класс, который может связывать рабочие потоки и запросы на закрытие пользовательского интерфейса. Он устанавливает себя в качестве фильтра событий в главном окне и откладывает его закрытие до тех пор, пока не завершатся все потоки.
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);
}
};
Наконец, у нас есть простой пользовательский интерфейс, который позволяет нам контролировать все это и видеть, что это работает. Ни при каких условиях пользовательский интерфейс не отвечает и не блокируется.
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
автоматически закрывается при уничтожении. Таким образом, все, что нам нужно сделать, чтобы закрыть порт, - это уничтожить его в нужном рабочем потоке, чтобы длительная операция не блокировала пользовательский интерфейс.
Таким образом, мы действительно хотим, чтобы объект (и его порт) были удалены из его потока (или утечки). Это просто достигается путем подключения 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);
//...
}