Размышление об Active Object в контексте Qt6. Часть 2.5

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

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Ссылки на статьи

  • Часть 1

  • Часть 2

  • Часть 2.5

Предисловие

Статья выпущена как дополнение к предыдущей и показывает, как можно сделать Active object, работающий асинхронно в среде Qt, но при этом не использующий события.

Иногда бывает слишком обременительно документировать использование кодов событий, поэтому желательно делать всё через сигналы.

Как это работает

Исходя из документации Qt, при вызове QObject::connect, можно указать Qt::ConnectionType. Если вкратце, то нас волнует возможность определять с помощью этого перечисление указывать, как вызывать слот.

  • Qt::DirectConnection — слот будет вызван напрямую при излучении сигнала, к которому он привязан. При этом обработка слота будет проводиться потоком, вызвавшим сигнал.

  • Qt::QueuedConnection — излучение сигнала будет преобразовано в событие, которое будет отправлено в очередь событий. Точно также, как мы это делали в предыдущей статье. Обработку слота при этом проводит поток-владелец QObject-а.

  • Qt::AutoConnection — если излучатель сигнала (sender) и его получатель (receiver) находятся в одном потоке, то слот этого QObject-а будет обработан напрямую потоком-владельцем. Если же они находятся в разных потоках, то сигнал пакуется в событие, которое помещается в очередь событий. Таким образом гарантируется, что слот всегда будет обрабатываться потоком-владельцем QObject-а.

  • Остальные перечислители рассматривать здесь я не буду, поскольку они не нужны нам в реализации класса Active object.

Нам как раз идеально подойдёт Qt::AutoConnection.

Реализуем

Для начала обернём задачу вывода на экран в целостную структуру. Эта структура является внутренней для класса SignalBasedAsyncQDebugPrinter, т.к. больше нигде не нужна. Получается практически тот же самый PrinterMessageEvent из предыдущей части.

PrinterMessage
class PrinterMessage {
private:
    QPromise<void> m_promise;
    const QString m_message;
public:
    PrinterMessage(const QString &message);
    const QString& message() const;
    QPromise<void>& promise();
};


SignalBasedAsyncQDebugPrinter::PrinterMessage::PrinterMessage(const QString &message)
    :m_message{ message } {}
const QString& SignalBasedAsyncQDebugPrinter::PrinterMessage::message() const {
    return m_message;
}
QPromise<void> &SignalBasedAsyncQDebugPrinter::PrinterMessage::promise() {
    return m_promise;
}

Теперь опишем сам SignalBasedAsyncQDebugPrinter. Реализация очень близка к EventBasedAsyncQDebugPrinter из предыдущего примера с той разницей, что используется сигнально-слотовое соединение, а значит, нужно иметь собственную очередь для передачи задач.

EventBasedAsyncQDebugPrinter
class SignalBasedAsyncQDebugPrinter : public QObject {
private:
    Q_OBJECT
    class PrinterMessage {/*...*/};
private:
    std::queue<PrinterMessage> m_messages;
    std::mutex m_mutex;
public:
    explicit SignalBasedAsyncQDebugPrinter(QObject *parent = nullptr);
    QFuture<void> print(const QString& message);
private slots:
    void handleNextMessage();
signals:
    void nextMessageReceived();
};



SignalBasedAsyncQDebugPrinter::SignalBasedAsyncQDebugPrinter(QObject *parent)
    :QObject{ parent } {
    QObject::connect(this, &SignalBasedAsyncQDebugPrinter::nextMessageReceived, this, &SignalBasedAsyncQDebugPrinter::handleNextMessage);
}

QFuture<void> SignalBasedAsyncQDebugPrinter::print(const QString &message) {
    auto task = PrinterMessage{ message };
    auto future = task.promise().future();
    {
        std::lock_guard locker{ m_mutex };
        m_messages.emplace(std::move(task));
    }
    emit this->nextMessageReceived();
    return future;
}
void SignalBasedAsyncQDebugPrinter::handleNextMessage() {
    std::queue<PrinterMessage> buffer;
    {
        std::lock_guard locker{ m_mutex };
        buffer.swap(m_messages);
    }

    while(not buffer.empty()) {
        qDebug() << buffer.front().message();
        buffer.front().promise().finish();
        buffer.pop();
    }
}

Для connect также существует перегрузка вида

connect(pointer, &SomeClass::signal, &SomeClass::slot);

Эта форма существует специально для связывания сигнала и слота одного и того же объекта. Но нам она не подходит, поскольку будет преобразована в

connect(pointer, &SomeClass::signal,pointer, &SomeClass::slot, Qt::DirectConnection);

В целом и полностью код похож на код event-based active object из предыдущей статьи.

В методе print мы создаём задачу, под блокировкой помещаем её в очередь, уведомляем сами себя через сигнал о том, что в очереди лежит новая задача и возвращаем промис на неё.

В методе handleNextMessage мы под блокировкой забираем всю очередь задач и обрабатываем её в цикле.

Немного о блокировках

Отдельно стоит отметить блокировку на 28-й строке в методе print. Если не снять блокировку до того, как вызвать emit this->nextMessageReceived();, то если метод print вызван потоком-владельцем SignalBasedAsyncQDebugPrinter, то тут же напрямую будет вызван метод handleNextMessage, который снова попытается захватить тот же самый мьютекс, в результате чего мы получаем deadlock.

Решить это можно несколькими путями.

Самый простой — в принципе запретить прямой вызов слота из сигнала. Для этого нужно вызывать метод connect в конструкторе с флагом Qt::QueuedConnection. Тогда даже независимо от того, какой поток вызвал print, слот будет вызван не сразу, а только по прошествии события через цикл событий.

QObject::connect с Qt::ConnectionType::QueuedConnection
QObject::connect(this, &SignalBasedAsyncQDebugPrinter::nextMessageReceived, &SignalBasedAsyncQDebugPrinter::handleNextMessage, Qt::ConnectionType::QueuedConnection);

Второй способ — тот, который представлен в реализации выше: просто обернуть lock_guard в лишний скоуп, что даст нам RAII0-разблокировку.

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

Реализация с двумя служебными методами
class SignalBasedAsyncQDebugPrinter : public QObject {
//Всё то же самое, что и раньше
//...
private:
    void emplaceTaskInQueue(PrinterMessage&& task);
    std::queue<PrinterMessage> takeTaskQueue();
};

QFuture<void> SignalBasedAsyncQDebugPrinter::print(const QString &message) {
    auto task = PrinterMessage{ message };
    auto future = task.promise().future();
    emplaceTaskInQueue(std::move(task));
    emit this->nextMessageReceived();
    return future;
}
void SignalBasedAsyncQDebugPrinter::handleNextMessage() {
    std::queue<PrinterMessage> buffer{ takeTaskQueue() };
    while(not buffer.empty()) {
        qDebug() << buffer.front().message();
        buffer.front().promise().finish();
        buffer.pop();
    }
}

void SignalBasedAsyncQDebugPrinter::emplaceTaskInQueue(PrinterMessage &&task) {
    std::lock_guard locker{ m_mutex };
    m_messages.emplace(std::move(task));
}

std::queue<SignalBasedAsyncQDebugPrinter::PrinterMessage> SignalBasedAsyncQDebugPrinter::takeTaskQueue() {
    std::queue<PrinterMessage> buffer;
    std::lock_guard locker{ m_mutex };
    m_messages.swap(buffer);
    return buffer;//NRVO, копирований нет
}

Заключение

Задача не использовать события зачастую возникает, когда вы пишете собственную библиотеку. Тогда, чтобы не пересекаться с событиями, определяемыми вашими пользователями, приходится вовсе отказываться от их использования.

Этот класс является повторением класса из предыдущей статьи, но не использует события, за счёт чего такая конструкция, возможно, станет более приемлемой в некоторых ситуациях.

Код примера, как обычно, на GitHub.

Источник: https://habr.com/ru/post/710550/


Интересные статьи

Интересные статьи

Данный материал - это вольный перевод онлайн-книги webrtcforthecurious.com. Вся книга будет разбита на отдельные статьи, которые будут выходить в разное время. В основном...
Эта статья представляет собой полное, подробное и исчерпывающее руководство по эффективному использованию Gatling для нагрузочного тестирования. Если вам нужно провести н...
Всем привет! Разработка гексапода активно продолжается и пришло время показать кардинальные изменения в конструкции и планы по прошивке. Появилась большая пауза в выходе новых статей в результа...
Попробуем подключить Celery/RabbitMQ к нашему проекту. В качестве основы возьмем проект с Flask'ом. Celery займется вычислением случайного числа.
Для меня большая честь присутствовать здесь, но, пожалуйста, не нужно меня взламывать. Компьютеры и так меня ненавидят, так что мне нужно подружиться с как можно большим числом людей в этом зале....