Как стать автором
Обновить

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

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

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

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

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

Подскажите стандартный виджет показа асинхронной операции.
НЛО прилетело и опубликовало эту надпись здесь

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


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

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

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», т.е. обработчик будет вызван еще один раз. У меня именно так и работает, и в этом нет ничего неожиданного.

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

Емнип Qt не даст работать с виджетами не в главном потоке. На счет QML не уверен, там все как-то хитро.
Эту задачу можно попробовать решить с помощью реактивного программирования, асинхронно обрабатывая поступающие данные (метод 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");});
Насколько я вижу библиотека rxcpp дает набор примитивов для асинхронных вычислений. Как я писал в статье, моя библиотека не завязана на конкретную асинхронную библиотеку и може истользоватся в том числе и с rxcpp.

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

Например в вашем примере я не вижу как можно отобразить прогресс асинхронной операции, как передать процент сделанного в этот виджет, так можно отменить операцию, если пользователь нажал кнопку Стоп или система знает, что результат операции уже не нужен.
Как-то все слишком сложно выглядит. Мьютексы, которые блокируются на момент вызова коллбэка…
Тут больше подойдет акторный подход, где каждый работает в своем потоке и никакие мьютексы не нужны.
А для того, чтобы не копировать значения каждый раз (хотя мне кажется это надуманной проблемой) можно изобрести COW
Как обойтись без локов, когда в одном потоке значение вычисляется, а в другом оно используется в виджете? Те же future/promise кажется используют мютексы, а тут «тяжеловесные » объекты GUI.

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

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

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

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

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


Просто мой класс AsyncValue в чем-то похож на std::future, в котором нет атомиков.
Насколько я теперь понимаю, вы предлагаете акторы с атомиками. Скорее всего там внутри будут какие-то очереди, динамические аллокации, чего хотелось бы избежать. Если я не прав, поправьте.
Вообще, поправьте меня, если я не прав, но сигналы/слоты с QueuedConnection само по себе асинхронны. Так что виджеты буду отображать асинхронные события. Или я опять не то понял?
Плюс, если использовать std::atomic, то точно ничего не сломается/не вернётся назад во времени или значении) Если же вам нужны контейнеры STL, то да, они не безопасны, но на то есть std::mutex, например.
Есть 2 потока, один поток с гуи, другой с бизнес-логикой. Поток с бизнес логикой будет большую часть времени ничего не делать, но при получении сигнала начнет работать над задачей. При этом так как весь поток бизнес-логи принадлежит классу бизнес-логики, то никаких мьютексов, атомиков и других средств синхронизации не требуется.
В процессе работы (и по окончании) поток бизнес логики посылает сигналы о результате выполнения операции. Чтобы было понятно, к какой операции пришел сигнал, можно передавать id операции.
То есть, мы запускаем операцию
b.runOperation(id);
он в ответ посылает сигналы
operationFinished(id);
Можно вместо id использовать более продвинутые подходы, например коллбэки
Есть 2 потока, один поток с гуи, другой с бизнес-логикой. Поток с бизнес логикой будет большую часть времени ничего не делать, но при получении сигнала начнет работать над задачей. При этом так как весь поток бизнес-логи принадлежит классу бизнес-логики, то никаких мьютексов, атомиков и других средств синхронизации не требуется.

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

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

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

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

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

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

я кажется понял вас, вместо одного объекта с синхронизацией его внутреннего состояния, вы предлагаете пересылать это внутренее состояние через параметр сигнала без всяких локов.
Тогда получается сам этот объект вырождается просто в сигнал. Это единственное, что он может выдать заинтересованным клиентам без локов. Но как клиены возьмут текущее состояние объекта без локов, когда сигнал ни разу еще не запустился? Как сделать wait?
А им не надо брать «текущее состояние». Это вообще вредная практика. Состояние, обновления и результат надо передавать исключительно через параметры сигналов, а подписчики в соседних тредах должны на эти сигналы реагировать. Реактивный подход, такскать.
Хорошо.
Как избежать ситуации, когда мы рассылаем сигнал с результатом, а в каком-то другом потоке изменили/пересчитали результат и нужно послать опять сигнал с новым результатом. Как запретить параллельные рассылки сигналов?
Ну как бы обычно смысл в том, что «пересчитать результат» в конкретном случае может только один поток, и он же после этого рассылает сигнал об изменении всем заинтересованным. Если какая-то большая задача параллелится, то мы разбиваем ее на независимые куски и эти куски раздаем в разные потоки, чтобы избежать излишних блокировок и улучшить data locality в плане кеширования, потом каждый поток оповестит заинтересованных через передачу сигнала с результатом. Как-то так.
Ну как бы обычно смысл в том, что «пересчитать результат» в конкретном случае может только один поток

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

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

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

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

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

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

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

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

Текущая реализация AsyncValue плоха хотябы тем, что там блокировки на любой чих. Вызвал коллбэк — заблокировался. Разослал уведомления — заблокировался. Это вызывает кучу проблем. Никогда не стоит вызывать пользовательский код под блокировкой.
Если хотябы от этих проблем есть возможность избавиться, возможно из AsyncValue и выйдет какой-то толк
НЛО прилетело и опубликовало эту надпись здесь
Я так понимаю что это нечто вроде примитива для разделения/передачи больших данных между потоками через сигналы/слоты без копирования (так-то в Qt можно прозрачным образом безопасно передать данные между потоками через queued connection, но передаваемые данные будут implicitly shared, а в худшем случае — таки и реально скопированы). Правда, в описании написано про виджет еще какой-то, но я так понимаю, что подойдет любой самописный, лишь бы его реализация могла что-то делать с ValueType/ProgressType/ErrorType.
Да, основной задачей изначально был унифицированный виджет для отображения асинхронной операции и результата её работы. Но более мясистым классом оказался AsyncValue.

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

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

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

Публикации

Истории