1

If I have a main thread and a separate, permanent worker QThread:

// Main thread where the event loop and QCoreApplication runs.
class NetworkController : public QObject {
public:
Q_OBJECT

    void connect_to_ap()
    {
        WifiAP *myAP = netMgr.get_best_ap();

        myAP->connect("my_psk_password");
    }

    // This is a class from a 3rd-party library, which contains, owns
    //  and manages the lifetime of "WifiAP" instances.
    NetworkManager netMgr;
};

// Separate thread where a state machine runs.
class StateMachineWorker : public QObject {
public:
Q_OBJECT

    void on_ready_to_connect_event()
    {
        // HERE: How to trigger NetworkController::connect_to_ap() and 
        // *block* until it returns.
    }

    NetworkController *pNetCtrlr;
}

When the state machine class enters a certain state it should connect to an AP (Access Point). The NetworkController has the functionality to connect to an AP.

I am trying to figure out a way that the state machine can do this in a thread-safe way. The problem is NetworkManager is always updating its list of WifiAP instances: they are created and destroyed often. It would not be thread-safe for StateMachineWorker to call pNetCtrlr->connect_to_ap() directly (as the NetworkManager in the NetworkController thread could at the same time delete the WifiAP instance).

So what I would like is in StateMachineWorker::on_ready_to_connect_event() to somehow signal the NetworkController to run its connect_to_ap() method in NetworkController's own thread, and to block the StateMachineWorker thread until connect_to_ap() has finished doing its stuff. The reason I want the state machine to be blocked is if I did not block and let it enter the event loop, it could receive some event that would make it transition to another state before connect_to_ap() has finished executing; this must not happen.

Mutex locks to protect the list of WifiAP in NetworkManager would not work as they would need to be inserted inside the 3rd-party library.

DBedrenko
  • 4,871
  • 4
  • 38
  • 73
  • Using a mutex?? ^^ – Robert Sep 28 '17 at 08:41
  • cond_var or mutex should do the work – rak007 Sep 28 '17 at 08:43
  • @Robert See my last sentence in the question. The mutex would need to be inside the 3rd-party lib, which I do not want to edit. There is no point mutex locking `netMgr`, because the `WifiAP` instances are deleted automatically inside `NetworkManager` (via signals/slots and what-not). – DBedrenko Sep 28 '17 at 08:43
  • Using wait() on StateMachineWorker thread ? – Amol Saindane Sep 28 '17 at 08:52
  • @SamuraiJack But I do not see how the `StateMachineWorker` can indicate to `NetworkController` thread that it should run `connect_to_ap()` in `NetworkController`'s thread, not the state machine's. – DBedrenko Sep 28 '17 at 08:54
  • 1
    `StateMachineWorker` can send the signal to `NetworkController` notifying that it should run `connect_to_ap` and then block itself using [QEventLoop](https://doc.qt.io/qt-5/qeventloop.html). You could connect the signal from `NetworkController` notifying about its job's end to the event loop's `quit` slot which would unblock `StateMachineWorker` and let it work further. For some details on `QEventLoop` usage see [this](https://stackoverflow.com/a/45331583/1217285) answer of mine. – Dmitry Sep 28 '17 at 08:57
  • QWaitCondition allows a thread to tell other threads that some sort of condition has been met. One or many threads can block waiting for a QWaitCondition to set a condition with wakeOne() or wakeAll(). Use wakeOne() to wake one randomly selected thread or wakeAll() to wake them all. – saeed Sep 28 '17 at 09:08
  • @rak007 Could you give an idea of how to do this in my case? @Dmitry thanks for the `QEventLoop` idea; it's a risky solution, from what I've read. I'll keep it in mind – DBedrenko Sep 28 '17 at 09:09
  • @DBedrenko This ? void on_ready_to_connect_event() { // Change your connect_to_ap return type to bool if you can std::packaged_task task(std::bind(this->pNetCtrlr->connect_to_ap)); std::future results = task.get_future(); task(); task.wait(); std::cout << std::boolalpha << "Everything went OK ? " << task.get() << std::endl; } – rak007 Sep 28 '17 at 09:31
  • @rak007 Thanks for your help, but wouldn't this run the `task` in a new thread that is concurrent to the `NetworkController` thread? That would mean the data race still exists. – DBedrenko Sep 28 '17 at 09:40
  • Well this is probably somthing that could happen – rak007 Sep 28 '17 at 10:04
  • @rak007 Yes, this is why I'm trying to execute `connect_to_ap()` in the `NetworkController` thread where we can work with `netMgr` and its `WifiAP` instances without threat of them being deleted or modified from another thread. – DBedrenko Sep 28 '17 at 10:07

1 Answers1

4

You can use QMetaObject::invokeMethod with parameter Qt::BlockingQueuedConnection. This connection type adds all the blocking logic for you and you don't have to change the third party library at all.

A general example:

objects.h

#include <QObject>
#include <QThread>
#include <iostream>

class Object : public QObject {
  Q_OBJECT

public slots:
  void foo() {
    std::cout << "Hello";
    thread()->msleep(2000);
    std::cout << " world!" << std::endl;
  }
};

class Caller : public QObject {
  Q_OBJECT

public:
  void call(Object* o) {
    std::cout << "Calling..." << std::endl;
    metaObject()->invokeMethod(o, "foo", Qt::BlockingQueuedConnection);
    std::cout << "Finished!" << std::endl;
  }
};

main.cpp

#include <QCoreApplication>
#include "objects.h"

int main(int argc, char* argv[])
{
  QCoreApplication a(argc, argv);

  QThread t;
  Object o;
  o.moveToThread(&t);

  t.start();

  Caller().call(&o);

  return a.exec();
}

The counterpart is that the method to be called must be a slot. If connect_to_ap is not already a slot you can create a bridge object that do the job, as explained below.

Have in mind that this bridge object must live in the same thread as the NetworkController (in your case the main thread), so the slot is queued in the correct events loop. You can take a look at QObject::moveToThread for further information.

A quick draft would be something like:

class NetworkControllerBridge : public QObject {
  Q_OBJECT

  NetworkController* nc;

public:
  NetworkControllerBridge(NetworkController* nc_) : nc(nc_) {}

public slots:
  void connect_to_ap() {
    nc->connect_to_ap();
  }
};

// ...

void on_ready_to_connect_event()
{
  NetworkControllerBridge bridge(pNetCtrlr);
  bridge.moveToThread(qApp->thread());
  metaObject()->invokeMethod(&bridge, "connect_to_ap", Qt::BlockingQueuedConnection);
}

Update

Another way to call a method through invokeMethod is to mark it as Q_INVOKABLE. Although you still need the bridge since you cannot modify the library, I mention this for completeness of the answer.

cbuchart
  • 10,847
  • 9
  • 53
  • 93