Согласование потоков Qt и не только. Активная 'посылка'


Решил поиграться с библиотекой Wt для написания веб сервисов на c++ и столкнулся с интересной задачей согласования произвольных потоков и Qt фреймворка, а точнее как доставить событие нажатия Wt::WButton в мой Qt::QWidget.

В общем случае задача проста - есть колбэк из произвольного(не Qt) потока и надо выполнить действие над обычным QObject‘ом.
Мне, почему-то, сразу пришла в голову следующая аналогия: Qt это огромный футуристический город из вращающихся по своим законам шестерёнок, в котором могут легко жить только исконно местные жители - КуЖители, общающиеся на своём ‘языке сигналов‘. А я(сторонний фреймворк) стою перед входом в город с сообщением, которое надо передать одному из этих жителей.
Теперь немного о том как это вообще можно сделать:

  1. Ринуться с посылкой прямо в город, рискуя быть перемолотым шестернями(нарваться на гонки потоков):
  2. MyQObject* object;
    wtbutton.clicked().connect(object, &MyQObject::doSomethig);
    //Или если используем лямбды:
    wtbutton.clicked().connect([object] () {
      object->doSomething();
      object->doSomethingAnother();
    });
  3. Создать QObject с необходимым сигналом, подключить его к слоту назначения и вызвать, отдав всю работу по согласованию Qt. В этом способе есть ряд недостатков:
     - если у меня есть 10 кнопок(Wt::WButton), которые уже имеют Wt-сигналы 'clicked', зачем плодить еще 10 Qt-сигналов buttonClicked()?
     - если мне надо не просто дернуть Qt-слот, а выполнить ряд действий над объектом?
  4. Использовать собственные велосипеды для синхронизации - мьютексы, вэйт-кондишены и пр.
Тут, думаю, можно еще предложить решения, но это уже в коментах. А я решил сделать 'посылку': идея в том, чтобы эта посылка сама была местным КуЖителем, дошла до назначения своим ходом и взорваласьсработала:
    MyQObject* object;
    wtbutton.clicked().connect([object] () {
      new Package(object, [object] () {
        object->doSomething();
        object->doSomethingOther();
      });
    });
Здесь колбэк 'clicked' срабатывает из стороннего потока, при этом создается объект посылки, которая доставляется в поток получателя, где выполняет собственно действия(открывает получатель посылку, а тут на тебе). Важно то, что поток вызывающий clicked() не блокируется и продолжает выполнение ДО того как посылка доставлена. Если надо блокировать то выглядит это так(можно думаю добавить еще сахару, но и так сойдет):
    MyQObject* object;
    wtbutton.clicked().connect([object] () {
      new Package(object, [object] () {
        object->doSomething();
        object->doSomethingOther();
      },
      Qt::BlockingQueuedConnection);
    });
Тут вызывающий поток заблокируется, подождет доставки посылки, и только потом продолжит выполнение.

Как это работает?
Package в конструкторе передает себя под управление потока, который обслуживает объект назначения. Затем он делает invokeMethod, а в методе исполняет переданную лямбду. QMetaObject::invokeMethod, аргументом принимает Qt::ConnectionType и таким образом можно сделать блокирующий вызов. А теперь сам код:
    class Package : public QObject {
      Q_OBJECT
        
    public:
      template <typename F>
      Package(QObject* recepient, const F& func, Qt::ConnectionType type = Qt::QueuedConnection)
        : func_(func)
      {
        moveToThread(recepient->thread());
        QMetaObject::invokeMethod(this, "fire", type);
      }
        
      virtual ~Package() {};
        
    private:
      Q_SLOT void fire() 
      {
        if (func_) func_();
        deleteLater();
      }
        
      std::function<void()> func_;
    };
    




Комментарии

Популярные сообщения из этого блога

Наконец-то GitHub Pages (и GitLab Pages — для симметрии)

SQLite в Docker: жить хорошо, а хорошо жить — ещё лучше (Litestream)

Чистка build-агентов Gitlab