Всем доброго времени суток. Хочу немного рассказать о своём проекте qt-async, возможно кому-то он покажется интересным или даже полезным.

Асинхронность и многопоточность уже давно и серьезно входит в обиход разработчиков. Многие современные языки и библиотеки разрабатываются с прицелом на асинхронное использование. Язык С++ тоже потихоньку движется в этом направлении — появились std::thread, std::promise/future, должны вот-вот завезти корутины и networking. Библиотека Qt тоже не отстает, предлагая свои аналоги QThread, QRunnable, QThreadPool, QFuture и т.п. При этом виджетов для отображения асинхронных действий в Qt я не нашел (возможно плохо искал, поправьте, если я ошибаюсь).

Поэтому я решил восполнить недостаток и попробовать реализовать такой виджет самому. Многопоточная разработка — дело сложное, но интересное.

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

Таким образом наша модель может находиться в одном из трех состояний:

  1. Progress — асинхронная операция выполняется
  2. Error — асинхронная операция завершилась ошибкой
  3. Value — асинхронная операция завершилась успешно

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

  1. В начале асинхронной операции перевести AsuncValue в состояние Progress
  2. В конце — либо в Error, либо в Value в зависимости от успешности операции
  3. Опционально в процессе выполнения операции можно обновлять Progress данные и слушать Stop флаг, если пользователь имеет возможность остановить операцию.

Вот схематичный пример с использованием QRunnable:

class MyRunnable : public QRunnable
{
public:
    MyRunnable(AsyncValue& value)
     : m_value(value)
    {}

    void run() final
    {
        m_value.setProgress(...);

        // do calculation

        if (success)
            m_value.setValue(...);
        else
            m_value.setError(...);
    }

private:
    AsyncValue& m_value;
}

Такая же схема для работы с std::thread:

AsyncValue value;
std::thread thread([&value] () {
    value.setProgress(...);

    // do calculation

    if (success)
        value.setValue(...);
    else
        value.setError(...);
});

Таким образом первая версия нашего класса могла бы выглядеть примерно так:

template <typename ValueType_t, typename ErrorType_t, typename ProgressType_t>
class AsyncValue
{
public:
    using ValueType = ValueType_t;
    using ErrorType = ErrorType_t;
    using ProgressType = ProgressType_t;

    // public API

private:
    QReadWriteLock m_lock;
    std::variant<ValueType, ErrorType, ProgressType> m_value;
};

Каждый, кто сталкивался с классами, которые поддерживают многопоточность, знает, что интерфейс таких классов отличается от однопоточных аналогов. Например функция size() бесполезна и опасна в многопоточном векторе. Её результат сразу же может стать невалидным, так как вектор может модифицироваться в данный момент в другом потоке.

Пользователи класса AsyncValue должны иметь возможность получить доступ к данным класса. Выдавать копию данных может быть накладно, любой из типов ValueType/ErrorType/ProgressType могут быть тяжеловесными. Выдавать ссылку на внутренние данные опасно — в любой момент они могут стать невалидными. Предлагается следующее решение:

1. Доступ к данным давать через функции accessValue/accessError/accessProgress, в которые передаются лямбды, принимающие соответствующие данные. Например:

template <typename Pred>
bool accessValue(Pred valuePred)
{
    QReadLocker locker(&m_lock);

    if (m_value.index() != 0)
        return false;

    valuePred(std::get<0>(m_value));
    return true;
}

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

2. Пользователь AsyncValue в функции accessValue может запомнить ссылку на внутренние данные, при условии, что он подписан на сигнал stateChanged и после обработки сигнала обязан больше не пользоваться этой ссылкой, т.к. она станет невалидной.

При таких условиях потребитель AsyncValue всегда гарантированно имеет валидный и удобный доступ к данным. У данного решения несколько последствий, влияющих на реализацию класса AsyncValue.

Во-первых, наш класс должен посылать сигнал при изменении состояния, но при этом он шаблонный. Придется добавить базовый Qt класс, где определить сигнал, по которому виджет будет обновлять свое содержимое, а все заинтересованные обновлять ссылки на внутренние данные.

сlass AsyncValueBase : public QObject
{
    Q_OBJECT
    Q_DISABLE_COPY(AsyncValueBase)

signals:
    void stateChanged();
};

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

Получается, что std::variant нам не подходит и придется хранить данные в динамической памяти, чтобы адреса новых и старых данных были неизменны.

Небольшое отступление.

Можно рассмотреть другие реализации класса AsyncValue, которые не требуют динамических аллокаций:

  1. Выдавать потребителям только копии внутренних данных AsyncValue. Как я писал раннее, такое решение может быть более неоптимально, если данные большие.
  2. Определить два сигнала вместо одного: stateWillChange/stateDidChange. Обязать потребителей по первому сигналу избавляться от старых ссылок и по второму сигналу получать новые ссылки. Данная схема, мне кажется, чрезмерно усложняет потребителей AsyncValue, т.к. у них появляются промежутки времени когда доступ к AsyncValue запрещен.

Получается следующая схематичная реализация функции setValue:

void AsyncValue::setValue(...)
{
    заблокировать m_lock на чтение
    скопировать указатели на старые внутренние данные в локальную переменную

    {
        повысить блокировку m_lock на запись
        поменять указатели на новые данные
        вернуть блокировку m_lock на чтение
    }

    разослать stateChanged сигнал
    удалить старые данные
    снять блокировку m_lock на чтение
};

Как видно, нам необходимо повышать блокировку m_lock на запись и возвращать обратно на чтение. К сожалению такой поддержки в классе QReadWriteLock нет. Достичь нужного функционала можно парой QMutex/QReadWriteLock. Вот реализация класса AsyncValue, приближенная к реальной:

// возможные состояния AsyncValue
enum class ASYNC_VALUE_STATE
{
    VALUE,
    ERROR,
    PROGRESS
};
Q_DECLARE_METATYPE(ASYNC_VALUE_STATE);

// базовый класс с сигналом и нешаблонными данными
class AsyncValueBase : public QObject
{
    Q_OBJECT
    Q_DISABLE_COPY(AsyncValueBase)

signals:
    void stateChanged(ASYNC_VALUE_STATE state);

protected:
    explicit AsyncValueBase(ASYNC_VALUE_STATE state, QObject* parent = nullptr);

    // пара локов для имитации PromoteToWriteLock/DemoteToReadLock
    QMutex m_writeLock;
    QReadWriteLock m_contentLock;
    // текущее состояние
    ASYNC_VALUE_STATE m_state;
};

template <typename ValueType_t, typename ErrorType_t, typename ProgressType_t>
class AsyncValueTemplate : public AsyncValueBase
{
    // данные
    struct Content
    {
        std::unique_ptr<ValueType_t> value;
        std::unique_ptr<ErrorType_t> error;
        std::unique_ptr<ProgressType+t> progress;
    };
    Content m_content;

public:
    using ValueType = ValueType_t;
    using ErrorType = ErrorType_t;
    using ProgressType = ProgressType_t;

    // создать новое значение
    template <typename... Args>
    void emplaceValue(Args&& ...arguments)
    {
        moveValue(std::make_unique<ValueType>(std::forward<Args>(arguments)...));
    }

    // положить новое значение
    void moveValue(std::unique_ptr<ValueType> value)
    {
        // локальная переменная для сохранения старых данных
        Content oldContent;

        // блокируем все emplaceXXX/moveXXX функции между собой
        QMutexLocker writeLocker(&m_writeLock);
        {
            // блокируем доступ к данным на запись
            QWriteLocker locker(&m_contentLock);

            // перемещаем указатели на старые данные
            oldContent = std::move(m_content);
            // выставляем новые данные
            m_content.value = std::move(value);
            // меняем состояние объекта
            m_state = ASYNC_VALUE_STATE::VALUE;

            // разблокируем доступ к данным
        }

        // рассылаем сигнал
        emitStateChanged();

        // снимаем блокировку для emplaceXXX/moveXXX функций
        // удаляем старые данные
    }

    // реализация аналогична value
    void emplaceError(Args&& ...arguments);
    void moveError(std::unique_ptr<ErrorType> error);
    void emplaceProgress(Args&& ...arguments);
    void moveProgress(std::unique_ptr<ProgressType> progress);

    template <typename Pred>
    bool accessValue(Pred valuePred)
    {
        // блокируем данные на чтение
        QReadLocker locker(&m_contentLock);

        // проверяем состояние объекта
        if (m_state != ASYNC_VALUE_STATE::VALUE)
            return false;

        // даем доступ к внутренним данным
        valuePred(*m_content.value);

        // снимаем блокировку с данных
        return true;
     }

    // аналогично accessValue
    bool accessError(Pred errorPred)
    bool accessProgress(Pred progressPred)
};

Для тех, кто не устал и не потерялся, продолжим.

Как можно заметить у нас есть функции accessXXX, которые не ждут пока AsyncValue перейдет в соответствующее состояние, а просто возвращают false. Иногда полезно синхронно дождаться пока в AsyncValue появится или значение или ошибка. По сути нам нужен аналог std::future::get. Вот сигнатура функции:

template <typename ValuePred, typename ErrorPred>
void wait(ValuePred valuePred, ErrorPred errorPred);

Для работы этой функции нам понадобится condition variable — объект синхронизации, который можно ждать в одном потоке и активизировать в другом. В функции wait нам следует ждать, а при смене состояния AsyncValue c Progress на Value или Error мы должны оповестить ждущих.

Добавление еще одного поля в класс AsyncValue, которое нужно в редких случаях, когда используют функцию wait, навела меня на размышления — можно ли сделать это поле опциональным? Ответ очевиден, конечно можно, если хранить std::unique_ptr и создавать его при необходимости. Возник второй вопрос — можно ли сделать это поле опциональным и не делать динамических аллокаций. Кому интересно, прошу посмотреть на следующий код. Основная идея в следующем, первый вызов wait создает на стеке структуру с QWaitCondition и записывает ее указатель в AsyncValue, последующие вызовы wait просто проверяют, если указатель не пустой, по пользуются структурой по этому указателю, если указатель пустой — см. выше про первый вызов wait.

class AsyncValueBase : public QObject
{
    ...
    struct Waiter
    {
        // основная переменная для ожидания 
        QWaitCondition waitValue;
        // счетчик вторичных wait
        quint16 subWaiters = 0;
        // первый wait должен ждать всех последующих
        QWaitCondition waitSubWaiters;
    };
    // указатель на структуру
    Waiter* m_waiter = nullptr;
};

    template <typename ValuePred, typename ErrorPred>
    void wait(ValuePred valuePred, ErrorPred errorPred)
    {
        // простой случай - мы получили значение или ошибку
        if (access(valuePred, errorPred))
            return;

        // блокируем AsyncValue от изменений
        QMutexLocker writeLocker(&m_writeLock);
        // проверяем простой случай снова
        if (access(valuePred, errorPred))
            return;

        // если данный вызов wait первый
        if (!m_waiter)
        {
            // создаем Waiter на стеке
            Waiter theWaiter;

            // выполняем этот код при выходе из if
            SCOPE_EXIT {
                // если есть последующие вызовы wait,
                // то они используют theWaiter 
                if (m_waiter->subWaiters > 0)
                {
                    // поэтому ждем пока subWaiters не обнулится
                    do
                    {
                         m_waiter->waitSubWaiters.wait(&m_writeLock);
                    } while (m_waiter->subWaiters != 0);
                }

                // больше никаких wait не осталось,
                // можно занулять поле и разрушать временный Waiter
                m_waiter = nullptr;
            };

            // выставляем адрес локального Waiter в AsyncValue
            // чтобы последующие вызовы wait использовали его
            m_waiter = &theWaiter;

            // ждем пока AsyncValue не перейдет в состояние Value или Error
            // и оповестит нас
            do
            {
                m_waiter->waitValue.wait(&m_writeLock);
            } while (!access(valuePred, errorPred));
        }
        // случай когда wait не первый
        else
        {
            // выполняем этот код при выходе из else
            SCOPE_EXIT {
                // убираем себя из счетчика ожидающих
                m_waiter->subWaiters -= 1;
                // если никого не осталось -> оповещаем первый wait
                if (m_waiter->subWaiters == 0)
                    m_waiter->waitSubWaiters.wakeAll();
            };

            // добавляем себя в счетчик ожидающих
            m_waiter->subWaiters += 1;

            // ждем пока AsyncValue не перейдет в состояние Value или Error
            // и оповестит нас
            do
            {
                m_waiter->waitValue.wait(&m_writeLock);
            } while (!access(valuePred, errorPred));
        }
    }

Как уже упоминалось, AsyncValue не имеет метода для асинхронных вычислений чтобы не привязываться к конкретной библиотеке. Вместо этого используются свободные функции, которые реализуют асинхронность тем или иным способом. Ниже приведён пример вычисления AsyncValue на пуле потоков:

template <typename AsyncValueType, typename Func, typename... ProgressArgs>
bool asyncValueRunThreadPool(QThreadPool *pool, AsyncValueType& value, Func&& func, ProgressArgs&& ...progressArgs)
{
    // создаём объект прогресс
    auto progress = std::make_unique<typename AsyncValueType::ProgressType>(std::forward<ProgressArgs>(progressArgs)...);
    //  запоминаем его адрес
    auto progressPtr = progress.get();

    // перемещаем прогресс в AsyncValue
    if (!value.startProgress(std::move(progress)))
        return false;

    QtConcurrent::run(pool, [&value, progressPtr, func = std::forward<Func>(func)](){
        SCOPE_EXIT {
            // в конце функции сообщаем AsyncValue, что прогресс завершён
            value.completeProgress(progressPtr);
        };

        // вычисляем AsyncValue
        func(*progressPtr, value);
    });

    return true;
}

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

Для повышения безопасности класс AsyncValue был расширен еще одним шаблонным классом AsyncTrackErrorsPolicy, который позволяет реагировать на неправильное использование AsyncValue. Например, вот реализация по умолчанию функции AsyncTrackErrorsPolicy::inProgressWhileDestruct, которая вызовется, если AsyncValue будет разрушаться во время работы асинхронной операции:

    void inProgressWhileDestruct() const
    {
        Q_ASSERT(false && "Destructing value while it's in progress");
    }

Что касается виджетов, то их реализация достаточно проста и лаконична. AsyncWidget — это контейнер, которые содержит в себе виджет для показа ошибки, либо прогресса, либо значения в зависимости в каком состоянии сейчас находится AsyncValue.

    virtual QWidget* createValueWidgetImpl(ValueType& value, QWidget* parent);
    virtual QWidget* createErrorWidgetImpl(ErrorType& error, QWidget* parent);
    virtual QWidget* createProgressWidgetImpl(ProgressType& progress, QWidget* parent);
 

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

Библиотека qt-async получилась компактной, но в то же время достаточно полезной. Использование AsyncValue/AsyncWidget, там где раньше были синхронные функции и статический GUI, позволит вашим приложениям стать современными и более отзывчивыми.

Для тех, кто прочитал до конца бонус — видео работы демо приложения

Комментарии (45)


  1. Sap_ru
    26.08.2019 04:43
    +1

    Но зачем?! Отправляешь событие в основной поток и там отображаешь в стандартном виджите же, нет?


    1. lexxmark Автор
      26.08.2019 04:59

      Ммм… Какое событие и в каком стандартном виджете? Не понял вопроса.


  1. lexxmark Автор
    26.08.2019 04:59

    удалено


  1. SR_team
    26.08.2019 08:08

    там же и так достаточно асинхронности. Рисующий поток отделен от потоков в которых обрабатываются клики и прочее


    1. lexxmark Автор
      26.08.2019 08:13

      Где «Рисующий поток отделен от потоков в которых обрабатываются клики»? Мне кажется это невозможно. По крайней мере в QtWidgets сообщения от виджетов обрабатываются в главном потоке.

      ам же и так достаточно асинхронности.

      Подскажите стандартный виджет показа асинхронной операции.


      1. Artifeks
        26.08.2019 11:11

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


      1. SR_team
        26.08.2019 12:32

        я когда инсталлер для рантайма писал, то в слоте, вызываемом кликом по кнопке в цикле проводил распаковку и вызывл обновление значения в прогрессбаре — интерфейс не зависал. В однопоточных тулкитах при таком сценарии интерфейс зависает и не обновляется, пока не завершится обработчик события по клику.


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


        1. lexxmark Автор
          26.08.2019 13:16

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


          1. KanuTaH
            26.08.2019 13:26

            Ну так запихивание в очередь событий и обработка этой очереди — это все-таки разные вещи. Вполне могу себе представить, что условный «драйвер мыши» запихивает в очередь событий новые клики, пока я обрабатываю старые. То же самое и с отрисовкой. Вот пример:

            import QtQuick 2.12
            import QtQuick.Window 2.12
            
            Window {
                visible: true
                width: 640
                height: 480
                title: qsTr("Hello World")
            
                MouseArea {
                    anchors.fill: parent
            
                    property string prop: ""
            
                    onClicked: {
                        console.log("CLICKED");
            
                        for (var i = 0; i < 10000000; i++) {
                            prop = "AAA";
                        }
            
                        console.log("DONE");
                    }
                }
            }
            


            Пока крутится цикл (не написало «DONE» в консоль), можно кликнуть по окну еще один раз, и после «DONE» сразу же напишет «CLICKED», т.е. обработчик будет вызван еще один раз. У меня именно так и работает, и в этом нет ничего неожиданного.


          1. SR_team
            26.08.2019 14:59

            в конструкторе окна делал (начледник QMainWindow)


          1. BeardedBeaver
            26.08.2019 19:01

            Емнип Qt не даст работать с виджетами не в главном потоке. На счет QML не уверен, там все как-то хитро.


  1. EkvVN
    26.08.2019 08:08

    Эту задачу можно попробовать решить с помощью реактивного программирования, асинхронно обрабатывая поступающие данные (метод on_next).
    А также обрабатывая случаи завершения (on_completed) и возникновения ошибки (on_error)
    в входной последовательности.
    Для C++ есть библиотека rxcpp.

    Псевдокод:

    auto srcValuesThread = rxcpp::observe_on_new_thread();
    auto uiThread = rxcpp::observe_on_new_thread();
    
    auto period = std::chrono::milliseconds(1);
    auto values = rxcpp::observable<>::interval(period);
    values		
    // отфильтровать только четные значения
    .filter([](int i){ return i % 2 == 0}) 
    // обработать только 3 значения		
    .take(3) 						
    // преобразование какждое значение	
    .map([](int i){ return i * 100});	
    // продолжить обработку на потоке uiThread
    .observeOn(uiThread) 				
    // выполнить подписку и запустить генерацию значений на потоке srcValuesThread
    .subscribe_on(srcValuesThread)		
    .subscribe(
        // on_next
        [this](int v){ this->labelWidget.setText("OnNext:");}
        // on_error
        [this](std::eception_ptr eptr){ this->labelWidget.setText("OnError"); },
        // on_completed
        [this](){ this->labelWidget.setText("OnCompleted");});
    


    1. lexxmark Автор
      26.08.2019 08:30

      Насколько я вижу библиотека rxcpp дает набор примитивов для асинхронных вычислений. Как я писал в статье, моя библиотека не завязана на конкретную асинхронную библиотеку и може истользоватся в том числе и с rxcpp.

      На мой взгляд ценность мое библиотеки в том, что она задает хорошие дефолты для состояние Progress и Error. И готовые виджеты, которые из коробки отобразят Progress и Error. Пользователю лишь надо указать как вычислять асинхронный результат (возможно и с помощью rxcpp) и предоставить метод-фабрику виджета, который отобразит этот результат. Вся остальная машинерия делается библиотекой.

      Например в вашем примере я не вижу как можно отобразить прогресс асинхронной операции, как передать процент сделанного в этот виджет, так можно отменить операцию, если пользователь нажал кнопку Стоп или система знает, что результат операции уже не нужен.


  1. svr_91
    26.08.2019 09:06

    Как-то все слишком сложно выглядит. Мьютексы, которые блокируются на момент вызова коллбэка…
    Тут больше подойдет акторный подход, где каждый работает в своем потоке и никакие мьютексы не нужны.
    А для того, чтобы не копировать значения каждый раз (хотя мне кажется это надуманной проблемой) можно изобрести COW


    1. lexxmark Автор
      26.08.2019 10:50

      Как обойтись без локов, когда в одном потоке значение вычисляется, а в другом оно используется в виджете? Те же future/promise кажется используют мютексы, а тут «тяжеловесные » объекты GUI.

      Можете привести схемотичный пример с акторами, хотелось бы посмотреть как там можно решить подобные проблемы.


      1. IGR2014
        26.08.2019 11:17

        Так а std::atomic?
        Помнится, в C++11 подвезли стандартизированную модель памяти с атомарными операциями которые потокобезопасны. Уже 2019-й и вышел C++17, так что 11-й стандарт вполне можно брать за минимум, если это не какой-то жуткий легаси, под который, кстати, Qt 5 тоже не соберётся, т.к. там как раз за основу C++11 принят


        1. lexxmark Автор
          26.08.2019 13:11

          Расскажите как атомики помогают реализовать ожидающий/блокирующий std::future:get()?


          1. IGR2014
            27.08.2019 18:01

            А зачем, если std::future:get() и так ожидающий/блокирующий?
            Зачем переизобретать велосипед?
            Или я не понял сути вашего вопроса?


            1. lexxmark Автор
              28.08.2019 09:19

              Я подумал, что ваш комментарий

              Так а std::atomic?

              относится к моему высказыванию
              Те же future/promise кажется используют мютексы,

              но наверное он относился ко второй части моего сообщения
              Можете привести схемотичный пример с акторами, хотелось бы посмотреть как там можно решить подобные проблемы.


              Просто мой класс AsyncValue в чем-то похож на std::future, в котором нет атомиков.
              Насколько я теперь понимаю, вы предлагаете акторы с атомиками. Скорее всего там внутри будут какие-то очереди, динамические аллокации, чего хотелось бы избежать. Если я не прав, поправьте.


              1. IGR2014
                28.08.2019 14:55

                Вообще, поправьте меня, если я не прав, но сигналы/слоты с QueuedConnection само по себе асинхронны. Так что виджеты буду отображать асинхронные события. Или я опять не то понял?
                Плюс, если использовать std::atomic, то точно ничего не сломается/не вернётся назад во времени или значении) Если же вам нужны контейнеры STL, то да, они не безопасны, но на то есть std::mutex, например.


      1. svr_91
        26.08.2019 14:41

        Есть 2 потока, один поток с гуи, другой с бизнес-логикой. Поток с бизнес логикой будет большую часть времени ничего не делать, но при получении сигнала начнет работать над задачей. При этом так как весь поток бизнес-логи принадлежит классу бизнес-логики, то никаких мьютексов, атомиков и других средств синхронизации не требуется.
        В процессе работы (и по окончании) поток бизнес логики посылает сигналы о результате выполнения операции. Чтобы было понятно, к какой операции пришел сигнал, можно передавать id операции.
        То есть, мы запускаем операцию
        b.runOperation(id);
        он в ответ посылает сигналы
        operationFinished(id);
        Можно вместо id использовать более продвинутые подходы, например коллбэки


        1. lexxmark Автор
          28.08.2019 09:28

          Есть 2 потока, один поток с гуи, другой с бизнес-логикой. Поток с бизнес логикой будет большую часть времени ничего не делать, но при получении сигнала начнет работать над задачей. При этом так как весь поток бизнес-логи принадлежит классу бизнес-логики, то никаких мьютексов, атомиков и других средств синхронизации не требуется.

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


          1. svr_91
            28.08.2019 10:14

            Сам поток бизнес-логики конечно может быть разбит и на несколько потоков. Да хоть на CUDA его напишите с миллионом потоков. Тут важнее взаимодействие между классом gui и классом бизнес-логики. И для класса gui класс бизнес-логики выглядит как однопоточный класс, в котором ничего нельзя изменить извне этого потока


            1. lexxmark Автор
              28.08.2019 10:33

              Тут важнее взаимодействие между классом gui и классом бизнес-логики.

              Предположим.
              И для класса gui класс бизнес-логики выглядит как однопоточный класс, в котором ничего нельзя изменить извне этого потока

              Как вы это гарантируете, кроме как на словах? И не сильное ли это ограничение?


              1. svr_91
                28.08.2019 10:42

                Я предложил это в первом сообщении — акторы. Потоки могут обмениваться друг с другом только сообщениями. Через сигналы и слоты например. В результате межпоточное взаимодействие (положить/извлечь сообщение в очередь) проявляется только в этом случае (и то в случае сигналов/слотов все уже реализовано в qt). Дальше поток разбирает сообщение и выполняет нужную работу в своем потоке когда и как закончит. По окончании (или в процессе обработки) посылает соообщения другим потокам (может быть тому, с которого ушло это сообщение) и так далее.


  1. RPG18
    26.08.2019 11:30

    При этом виджетов для отображения асинхронных действий в Qt я не нашел (возможно плохо искал, поправьте, если я ошибаюсь).

    Стандартные виджеты из коробки "асинхронные". Стандартные виджеты поддерживают сигналы/слоты, которые чудесно работают между потоками. А там где у нас нет сигналов(std::thread, QRunnable и т.д.) мы всегда можем дернуть слот через QMetaObject::invokeMethod c Qt::QueuedConnection.


    1. lexxmark Автор
      26.08.2019 13:10
      -1

      Слоты конечно потокобезопасные, но их недостаточно.
      Я описал одну из проблем, вот у вас прогресс идет и асинхронная операция закончилась, надо удалить прогресс и положить вместо него результат операции, но пока идет рассылка сигналов, удалять прогресс нельзя, то есть нужно держать и прогресс и результат одновременно. И заменять одно на другое, очевидно, под локом


      1. RPG18
        26.08.2019 13:13

        надо удалить прогресс и положить вместо него результат операции

        Заводишь третий сигнал finished, в котором передаешь вычисленное значение(передавать можно как по значению, так и по указателю). Соответственно у вас все происходит атомарно и без локов.


        1. lexxmark Автор
          26.08.2019 13:31

          я кажется понял вас, вместо одного объекта с синхронизацией его внутреннего состояния, вы предлагаете пересылать это внутренее состояние через параметр сигнала без всяких локов.
          Тогда получается сам этот объект вырождается просто в сигнал. Это единственное, что он может выдать заинтересованным клиентам без локов. Но как клиены возьмут текущее состояние объекта без локов, когда сигнал ни разу еще не запустился? Как сделать wait?


          1. KanuTaH
            26.08.2019 13:39

            А им не надо брать «текущее состояние». Это вообще вредная практика. Состояние, обновления и результат надо передавать исключительно через параметры сигналов, а подписчики в соседних тредах должны на эти сигналы реагировать. Реактивный подход, такскать.


            1. lexxmark Автор
              28.08.2019 09:32

              Хорошо.
              Как избежать ситуации, когда мы рассылаем сигнал с результатом, а в каком-то другом потоке изменили/пересчитали результат и нужно послать опять сигнал с новым результатом. Как запретить параллельные рассылки сигналов?


              1. KanuTaH
                28.08.2019 09:51

                Ну как бы обычно смысл в том, что «пересчитать результат» в конкретном случае может только один поток, и он же после этого рассылает сигнал об изменении всем заинтересованным. Если какая-то большая задача параллелится, то мы разбиваем ее на независимые куски и эти куски раздаем в разные потоки, чтобы избежать излишних блокировок и улучшить data locality в плане кеширования, потом каждый поток оповестит заинтересованных через передачу сигнала с результатом. Как-то так.


                1. lexxmark Автор
                  28.08.2019 10:18

                  Ну как бы обычно смысл в том, что «пересчитать результат» в конкретном случае может только один поток

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


                  1. KanuTaH
                    28.08.2019 10:38

                    А причём тут пул потоков? Он нужен только лишь для того, чтобы не пересоздавать поток каждый раз, и все. На независимость задач это не влияет. Как только поток заканчивает расчёт своей независимой части, он посылает соответствующий сигнал, скажем, finished(), так вы и узнаете, закончен он или нет. Я что-то не понимаю проблемы пока.


                    1. svr_91
                      28.08.2019 10:44

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


                      1. KanuTaH
                        28.08.2019 10:45

                        Ну да. Бывают, конечно, задачи, где такое затруднительно, но в основном все-таки декомпозиция обычно получается.


                        1. lexxmark Автор
                          28.08.2019 11:14

                          Да, если встать на вашу точку зрения, похоже AsyncValue решает две задачи. Возможно удастся его декомпозировать, главное не переусложнить.

                          По моему мнению библиотечные классы должны быть просты и удобны в использовании. Реализация может быть сложноватой (на то она и библиотека) с понятными накладными расходами на использование. При этом желательно, чтобы неправильное использование класса приводило к ошибкам компиляции или хотя бы рантайм проверкам.

                          Таким образом появился AsyncValue класс — такой std::future на стероидах (на мой взгляд). Ну а виджеты — простое и удобное дополнение к ним.


                          1. KanuTaH
                            28.08.2019 11:23

                            На самом деле Qt имеет все механизмы для комфортной и прозрачной асинхронной работы, как примитивы (сигналы/слоты), так и вещи по управлению потоком выполнения асинхронных задач (QFuture и иже с ним).


                          1. svr_91
                            28.08.2019 11:28

                            Текущая реализация AsyncValue плоха хотябы тем, что там блокировки на любой чих. Вызвал коллбэк — заблокировался. Разослал уведомления — заблокировался. Это вызывает кучу проблем. Никогда не стоит вызывать пользовательский код под блокировкой.
                            Если хотябы от этих проблем есть возможность избавиться, возможно из AsyncValue и выйдет какой-то толк


  1. Artifeks
    26.08.2019 11:43

    Спасибо что поделились своим проектом, и рад как фанат Qt, что сообщество не дремлет. Но я не совсем понял какую проблему решает ваша библиотека. Т.к. в Qt и так есть асинхроность с сигнал слотами и т.д.


    1. KanuTaH
      26.08.2019 12:18

      Я так понимаю что это нечто вроде примитива для разделения/передачи больших данных между потоками через сигналы/слоты без копирования (так-то в Qt можно прозрачным образом безопасно передать данные между потоками через queued connection, но передаваемые данные будут implicitly shared, а в худшем случае — таки и реально скопированы). Правда, в описании написано про виджет еще какой-то, но я так понимаю, что подойдет любой самописный, лишь бы его реализация могла что-то делать с ValueType/ProgressType/ErrorType.


      1. lexxmark Автор
        26.08.2019 12:53

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

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


    1. lexxmark Автор
      26.08.2019 13:04

      С помощью таких AsyncValue очень удобно строить внутреннюю логику программы, выстраивать цепочки зависимых значений. А с помощью готовых виджетов очень легко поверх этого строить GUI. Там где должен лежать виджет результата асинхронной операции просто кладем AsyncWidget и он сам покажет прогресс или ошибку или ваш виджет с готовым результатом.

      Библиотека — попытка обобщить такой подход и выделить повторяющийся код.

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


  1. Tantrido
    26.08.2019 20:36

    Закинь статью в Qt mailing list (на английском) и скинь сюда ссылку, пожалуйста,- интересно мнение разработчиков Qt.


    1. lexxmark Автор
      28.08.2019 09:33

      Если будет свободное время — попробую