Worker threads with shared resources in Qt application

╄→尐↘猪︶ㄣ 提交于 2019-12-13 00:24:00

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!