Исследую паттерны thread pool и воркеры,
Прикручиваю асинхронность к UI приложению.
Анализирую как бы лучше оформить архитектуру, в осн. тут мысли вслух, и попытка сформулировать дизайн требования.
Если любите изобретать велики — велком. Нервных — просьба без валидола не смотреть
Исходные условия:
Все объекты, связанные с UI — например ViewController, или виджеты типа View, Button, Slider — хранятся в дереве оконной иерархии по shared_ptr.
Контроллер — корневой объект иерархии — создает свое корневое окно и сильно владеет им, в окне — размещаются главные зоны, которые размещают свои дочерние виджеты и тд.
(Дети в иерархии могут ссылаться обратно на родителей по weak_ptr, если нужно)
Внутри любого метода, например, Button::onPressed() бывает нужно запустить цепочку асинхронных (неблокирующих) операций,
но тем не менее связанных друг с другом, последовательно.
Например, закинуть запрос в сеть -> дождаться ответа -> по ответу подгрузить из файла текстурку и декодить — по готовности -> отобразить на экране изменения
Экранную иерархию можно трогать только из main thread.
Собственно, начинаю с рассуждений о дизайне воркеров.
Хотелось бы такой апи (псевдокод):
class Button : View {
Label label;
}
void Button::onPressed()
{
Pool::dispatch([=]() // to worker thread
{
auto answer = Network::get(url); // допустим тут везде блокирующие апи, не суть важно тк мы в воркер треде
auto result = Parser::parse(answer);
Pool::dispatch([=]() {
this->label.setText(to_string(result));
});
});
}
Первое. Капчуры []
Чтобы сделать this->label во вложенной лямбде, надо везде захватывать this. Но это очень опасно!
Button уже может уйти в небытие — т.к. пока грузится сеть, пользователь может уйти со вкладки и родитель грохнетcя вместе с Button
Т.е нужно использовать weak_ptr
Т.е Button (а вернее, вся оконная иерархия View) — должна наследоваться от enable_shared_from_this()
чтобы в своих методах иметь доступ к своему shared_ptr
Второй вопрос, а что если где то другой воркер полезет в label.setText()
не защищать же сам метод мьютексом что приведет к простою воркеров.
Значит нужен отдельный вид dispatch для main thread который будет их складывать в очередь и выгребать серийно,
например в начале очередного цикла обработки сообщений (runloop)
class View : enable_shared_from_this() {};
class Button : View { Label label; };
void Button::onPressed() {
auto shared = shared_from_this();
Pool::dispatch([weak_ptr weakThis = shared ]() // захватываем weak по значению
{
auto answer = Network::get(url);
auto result = Parser::parse(answer);
Pool::dispatch_main([weakThis]() { // to main thread
if (auto strongThis = weakThis.lock()) // есть ли кнопка?
{
this->label.setText(to_string(result));
}
});
});
}
Лямбды, которые захватывают слишком много могут быть тяжеловесными,
возникает вопрос — как эффективнее их хранить
Ведь бывает нужно захватить не только weak_ptr, а несколько других связанных объектов UI, текущую открытую базу, текущий сетевой запрос и тд
Т.к синглтоны в примере — для простоты, в реальной аппке там будет довольно длинный капчер полноценных объектов / их shared_ptr
Плюс, очень не нравится этот танец weak — strong, который нужно делать каждый раз вручную, чтобы заблокировать shared из weak
Интересно, есть ли какой то удобный враппер умеющий это делать 1 строчкой элегантно,
Было бы идеально так:
checked(weakPtr)->setText();
Знаю, так нельзя синтаксически но смысл такой — checked принимает weak делает ему lock и если там nullptr то ничего не делается,
а если там shared_ptr делается вызов оператора структурного дереференса -> и вызов метода setText() у обернутого типа (Button)
можно сделать с лямбдой но будет больше кода чем просто if (auto strong = weak.lock) {}