Архитектура с воркерами и threadpool для UI #1 (велосипед!)
От: barney  
Дата: 17.05.18 17:05
Оценка:
Исследую паттерны 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) {}
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.