Qt's Signals / Slots временное одноразовое соединение - PullRequest
0 голосов
/ 14 сентября 2018

У меня есть проблема, может быть, вы можете мне помочь.

В определенном потоке есть клиент amqp, который связывается с сервером rabbitmq.

Мне нужно заблокировать (и проверитьесли он уже заблокирован) некоторый ресурс, прежде чем клиент начнет его использовать.LockResource является важным предварительным условием для различных функций.

В первом примере я отключаю последнюю подключенную lamba, но происходит странное поведение, иногда вызывается do_stuff (), иногда нет ...

Во втором примере все права, но это убивает многопоточность.

В третьем, чтобы избежать утечки памяти, мне нужно изменить AmqpClient, чтобы он всегда выдавал resourceLocked с «магическим числом», когда естьошибка и проверьте ее перед вызовом do_stuff () ... Это совсем нехорошо ...

Может быть, я что-то не так делаю или неправильно понял.Если у вас есть лучший способ, я беру его.


РЕДАКТИРОВАТЬ 16/09/2018

Я ввел вас в заблуждение неточными объяснениями: do_stuff () не уникальный метод, но различные инструкциииначе я бы подключил это напрямую.Было бы лучше написать […] вместо do_stuff ().Также есть только уникальный клиент по экземпляру.

Я не могу заранее знать, что пользователь выполнит первым.Он может заблокировать ресурс для readResourceContent, deleteResource, writeResourceProperty,…, так что все это должно выполнять разные инструкции.

Хорошо, что благодаря вашим ответам у меня есть рабочее решение, использующее bool QMetaObject::invokeMethod(QObject *context, Functor function, Qt::ConnectionType type = Qt::AutoConnection, FunctorReturnType *ret = nullptr)

Общие декларации

using Callback = std::function<void()>;
Q_DECLARE_METATYPE(Callback)

qRegisterMetaType<Callback>("Callback");

AmqpClient.cpp

void AmqpClient::lockResource(Identifier identifier, QObject *context, const Callback &func)
{
    if(lockedResources_.contains(identifier))
    {
        QMetaObject::invokeMethod(context,
                                  func,
                                  Qt::QueuedConnection);
        return;
    }

    QString queueName(QString::number(identifier) + ".lock");
    QAmqpQueue *lockQueue = client_->createQueue(queueName);

    connect(lockQueue, qOverload<QAMQP::Error>(&QAmqpQueue::error), this, [this](QAMQP::Error error) {
        if(error == QAMQP::ResourceLockedError) {
            emit errorMessage("The expected resource is already locked by another user.");
            sender()->deleteLater();
        }
    });

    connect(lockQueue, &QAmqpQueue::declared, this, [=]() {
        QAmqpQueue *lockQueue = qobject_cast<QAmqpQueue*>(sender());
        lockQueue->consume(QAmqpQueue::coExclusive);
        lockedResources_[identifier] = lockQueue;

        QMetaObject::invokeMethod(context,
                                  func,
                                  Qt::QueuedConnection);
    });

    lockQueue->declare(QAmqpQueue::Exclusive | QAmqpQueue::AutoDelete);
}

Controller.cpp

void Controller::readResourceContent(int row) 
{    
    [...]
    QMetaObject::invokeMethod(amqp_,
                              "lockResource",
                              Qt::AutoConnection,
                              Q_ARG(Identifier, identifier),
                              Q_ARG(QObject*, this),
                              Q_ARG(Callback, [&](){ [...] }));
    [...]
}

1

// called not inside connect(...) because it may not to emit the signal 
// (if resource is already locked)
disconnect(amqp_, &AmqpClient::resourceLocked, 0, 0);

connect(amqp_, &AmqpClient::resourceLocked, this, [&](){
  do_stuff();
});

emit lockResource(identifier, QPrivateSignal());

2

// This is working like a charm, but I'm losing ui reactivity
QEventLoop loop;
connect(amqp_, &AmqpClient::resourceLocked, &loop, &QEventLoop::quit);
emit lockResource(identifier, QPrivateSignal());
loop.exec();
do_stuff();

3

// Using an intermediate object
class CallbackObject : public QObject
{
    Q_OBJECT
    std::function<void()> callback;

public:
    CallbackObject(std::function<void()> callback) : QObject(), callback(callback) {}

public slots:
    void execute() { callback(); deleteLater(); }
};

// Working but memory leak if signal is not emitted 
// resource already locked for example
CallbackObject *helper = new CallbackObject([&](){
  do_stuff() ;
});
connect(amqp_, &AmqpClient::resourceLocked, helper, &CallbackObject::execute);
emit lockResource(identifier, QPrivateSignal());

1 Ответ

0 голосов
/ 15 сентября 2018

Если я правильно вас понимаю, вы пытаетесь настроить следующую последовательность событий:

          Client                          AMQPClient
             |       lockResource(id)        |
             |------------------------------>|
             |                               |
             |                               |--\
             |                               |   | 
             |        resourceLocked         |   | try to acquire resource
             |<------------------------------|   |
          /--|                               |<-/
do_stuff |   |                               |
          \->|                               |
             |                               |

, где сообщение resourceLocked не отправляется, когда AMQP не может получить ресурс и, таким образом, do_stuff также не вызывается в этом случае.

Проблема с do_stuff, которая не вызывается при использовании реализации 1, связана с состоянием гонки, когда несколько клиентов ожидают одновременной блокировки соответствующего ресурса. Одна последовательность, демонстрирующая описанную вами проблему (плюс дополнительные проблемы с этим подходом, а также с вашей реализацией 3), выглядит следующим образом:

  1. Клиент A входит в функцию 1: он отключает все существующие подключения к AmqpClient::resourceLocked, а затем подключается к этому сигналу.
  2. Клиент B теперь также входит в функцию 1: он также отключает все существующие соединения с AmpqClient::resourceLocked, в частности, соединение, только что установленное клиентом A. Затем он подключается к этому сигналу.
  3. AmpqClient обрабатывает запрос клиента lockResource. Он получил запрошенный ресурс и издает сигнал resourceLocked.
  4. Клиент B, единственный подключенный к сигналу, теперь выполняет do_stuff, хотя это был запрошенный клиентом ресурс.
  5. AmpqClient обрабатывает запрос клиента B lockResource. Опять же, он успешно получает ресурс и испускает сигнал resourceLocked.
  6. Клиент B, все еще подключенный к этому сигналу, снова выполняет do_stuff.

Вышеприведенная последовательность показывает, что do_stuff клиента A, который не вызывается, не так уж и плох, учитывая, что клиент B будет работать, пока его ресурс еще не получен.

Чтобы исправить это, вы должны убедиться, что вы только do_stuff звоните клиенту, чей запрос lockResource был только что обработан. Сигнал, который по замыслу всегда уведомляет всех наблюдателей, является просто неоптимальным подходом, поскольку тогда наблюдатели должны будут проверить, должен ли сигнал уведомлять их или кого-либо еще.

Первым исправлением является изменение сигнала lockResource, чтобы он также отправлял указатель this клиента, который излучает сигнал. Таким образом, AmqpClient может использовать этот указатель для обратного вызова клиента, когда он получил ресурс.

Одна реализация слота, подключенного к сигналу, может выглядеть так:

void AmqpClient::handleLockResourceRequest(int identifier,
                                           QObject* requestingClient)
{
   // try to acquire resource described by `identifier`

   if (resource_acquired_successfully)
   {
       QMetaObject::invokeMethod(requestingClient, "do_stuff", Qt::AutoConnection);
   }
 }

Обратите внимание, что для работы invokeMethod, do_stuff должен быть либо слотом, либо должен быть помечен как Q_INVOKABLE.

Но я бы пошел на шаг дальше, чем просто вызвать do_stuff через invokeMethod: Насколько я могу сказать, AmqpClient - единственный слушатель, подключенный к lockResource, и эти два объекта были в тот же поток, который вы, вероятно, просто использовали бы прямой вызов amqp_->tryToLockResource(identifier) вместо emit lockResource(identifier). То есть единственная причина, по которой вы используете сигнал, заключается в том, что вызов проходит через цикл обработки событий.

Вместо использования сигнала вы также можете использовать invokeMethod для запроса получения ресурса через цикл обработки событий. Преимущество этого в том, что ваш клиентский класс больше не раскрывает тот факт, что ему необходимо приобретать ресурсы, чтобы выполнять свою работу пользователям этого класса.

В итоге полученный код будет выглядеть примерно так:

class ClientType : public QObject {
  Q_OBJECT
public:
  // ...

  Q_INVOKABLE void do_stuff(); // definition as before

private:
   void requestResource(); // was previously code block 1 in your question

private:
  AmqpClient* amqp_;
  // ...
};

inline void ClientType::requestResource() 
{
  auto identifier = ...; // create resource identifier

  QMetaObject::invokeMethod(amqp_, 
                            "requestResource", 
                            Qt::AutoConnection, 
                            Q_ARG(int, identifier),
                            Q_ARG(QObject*, this));
}



class AmqpClient : public QObject {
  Q_OBJECT
public:
  // ...

  Q_INVOKABLE requestResource(int identifier, QObject* requestingClient); 

};

inline void AmqpClient::requestResource(int identifier,
                                        QObject* requestingClient)
{
   // try to acquire resource described by `identifier`

   if (resource_acquired_successfully)
   {
       QMetaObject::invokeMethod(requestingClient, "do_stuff", Qt::AutoConnection);
   }
}

В приведенной выше реализации я предположил, что идентификаторы ресурсов имеют тип int, но, конечно, вы можете использовать любой другой тип, зарегистрированный в системе метатипов Qt. Аналогично, вместо передачи QObject -пунктов вы также можете использовать ClientType* (или абстрактный базовый класс, чтобы избежать введения циклических зависимостей) после регистрации типа указателя в системе метатипов Qts.

...