问题
I am working on a Qt application which involves serial communication with one or multiple devices. There are different procedures that can be executed simulteanously and each procedure may send one or unknown number of commands to a device and may receive data in response. To make it more clear, here is a graphical illustration of the scenario:
Clicking on a button triggers the execution of the corresponding procedure. So two or more different procedures may be running at the same time when the user clicks on two or more buttons in a short interval. Actually the only thing that may be shared between them is the serial communication with a single device; otherwise they are mostly independent of one another. And here are two pseudo-code examples of what a procedure may look like:
Procedure A:
begin
write command a1 on serial port
wait for one second
perform some computations
write command a2 on serial port
wait for one second
end
Procedure B:
begin
while true:
write command b1 on serial port
read the response from serial port
perform some computations
if a condition holds return, otherwise continue
end
My solution and its issue:
To simplify the situation consider that there is only one device which we need to communicate with. Since procedures can be executed simulteanously (and only one of them can communicate with the device through serial port at a time) I have created one thread and one worker class for each of the procedures and have moved the workers to their corresponding threads. To synchronize procedures when accessing the serial port I have created one mutex:
MainWindow.h
class MainWindow : public QMainWindow {
public:
//...
QSerialPort* serial_;
QMutex serial_mutex_;
private:
//...
ProcAWorker* proca_worker;
ProcBWorker* procb_worker;
ProcCWorker* procc_worker;
ProcDWorker* procd_worker;
QThread proca_thread;
QThread procb_thread;
QThread procc_thread;
QThread procd_thread;
}
MainWindow.cpp
void MainWindow::onConnectButtonClicked()
{
serial_ = new QSerialPort();
// configure serial port settings
serial_->open(QIODevice::ReadWrite);
}
void MainWindow::onButtonAClicked()
{
proca_worker = new ProcAWorker(0, this); // pass a pointer to this class to be able to access its methods and members
proca_worker->moveToThread(&proca_thread);
// setup worker-thread connections: started, quit, finished, etc.
proca_thread.start(); // triggers `proccess` slot in proca_worker
}
// same thing for other buttons and procedures
ProcAWorker.cpp
void ProcAWorker::ProcAWorker(QObject *parent, QMainWindow *wnd) :
QObject(parent), wnd_(wnd)
{
}
void ProcAWorker::process()
{
wnd_->serial_mutex_->lock();
wnd_->serial_->write('Command a1'); // Warning occurs in this line
bool write_ok = client_->serial_->waitForBytesWritten(SERIAL_WRITE_TIMEOUT);
wnd_->serial_mutex_->unlock();
QThread::sleep(1);
// perform some computations
wnd_->serial_mutex_->lock();
wnd_->serial_->write('Command a2');
bool write_ok = client_->serial_->waitForBytesWritten(SERIAL_WRITE_TIMEOUT);
wnd_->serial_mutex_->unlock();
if (write_ok) {
// signal successful to main window
emit success();
}
}
However, when the write operation is performed on the serial port (i.e. wnd_->serial_->write('Command a1');
) the following warning is shown:
QObject: Cannot create children for a parent that is in a different thread. (Parent is QSerialPort(0x18907d0), parent's thread is QThread(0x13cbc50), current thread is QThread(0x17d8d08)
My questions:
1) I have already looked at other questions on Stackoverflow regarding this warning, but their answers have only mentioned that signal/slot should be used. I am familiar with using signal/slot to communicate with worker threads. However, I can't figure out how to implement my specific scenario (simultaneous running procedures with shared resources like serial port) using signal/slot or how can I modify my current solution to resolve this issue? Note that the procedures should be allowed to run in parallel (unless in those moments when they want to communicate with the device). Obviously one can run the procedures sequentially (i.e. one after another) but I am not looking for such solutions.
2) Actually there is also a "Halt" button that stops all the running procedures and sends a halt command to the device. But I could not figure out to implement this functionality as well (set a flag, send a quit signal, etc.). Could you please give me some hints in this regards as well?
回答1:
First of all, you don't need explicit multithreading (it's optional), second of all you don't need any manually managed synchronization primitives.
Then, model each procedure using a state machine. Hopefully the communication protocol allows each procedure recognize the responses to its own commands, so that even though you'd be replicating the incoming data to all of the procedures, they'd ignore the data irrelevant to them.
This answer has a sketch of a solution that does exactly what you want, sans multiplexing. Multiplexing a QIODevice
is trivial when you expose it via local pipes: everything incoming from the port is written to one end of one or more local pipes. Everything incoming from the pipes is written to the port. The pipes will maintain the integrity of the packets as long as you open their procedure end in Unbuffered
mode. That way each write
will arrive at the serial port as a contiguous block of bytes, and will be written to the port in the same manner.
How would you multiplex? Like so:
class IODeviceMux : public QObject {
Q_OBJECT
QVector<QPointer<AppPipe>> m_portPipes;
QVector<QPointer<AppPipe>> m_userPipes;
QPointer<QSerialPort> m_port;
public:
IODeviceMux(QObject *parent = {}) : QObject(parent) {}
void setPort(QIODevice *port) {
if (m_port) {
disconnect(m_port.get(), 0, this, 0);
m_userPipes.removeAll({});
for (auto pipe : qAsConst(m_userPipes))
disconnect(m_port.get(), 0, pipe.get(), 0);
}
m_port = port;
connect(m_port.get(), &QIODevice::readyRead, this, &IODeviceMux::onPortRead);
}
AppPipe *getPipe() {
QScopedPointer<AppPipe> user(new AppPipe(QIODevice::ReadWrite | QIODevice::Unbuffered));
auto *port = new AppPipe(QIODevice::ReadWrite | QIODevice::Unbuffered, this);
user->addOther(port);
connect(port, &QIODevice::readyRead, this, &IODeviceMux::onPipeRead);
connect(m_port.get(), &QIODevice::bytesWritten, user.get(), &QIODevice::bytesWritten);
connect(user, &QObject::destroyed, port, &QObject::deleteLater);
m_userPipes.push_back(user.get());
m_portPipes.push_back(port);
return user.take();
}
private:
void onPortRead() {
if (!m_port) return;
auto data = m_port->readAll();
m_portPipes.removeAll({});
for (auto pipe : qAsConst(m_portPipes))
pipe->write(data);
}
void onPipeRead() {
auto *pipe = qobject_cast<AppPipe*>(sender());
QByteArray data;
if (pipe) data = pipe->readAll();
if (m_port) m_port->write(data);
}
};
The procedures would each getPipe()
and treat the pipe as if it was a serial port device. Each write
into a pipe gets faithfully executed on the port. Each readyRead
on the port is faithfully forwarded, with same data amounts available immediately to read. Even the port's bytesWritten
is forwarded. But bytesToWrite
doesn't work - it always returns zero. This could be fixed by adding an option to AppPipe
to query this value.
That's about all you need to get it to work, I'd think.
来源:https://stackoverflow.com/questions/51838526/worker-threads-with-shared-resources-in-qt-application