Коротко не очень об асинхронности в dart (с примерами в Flutter). Статья написана в 2024 году. Актуальная версия Flutter 3.16.9 и Dart 3.2.6. Код на github. Асинхронное исполнения кода в Dart(Flutter) здесь описано с прицелом на программистов, приходящих в Flutter из других языков. Чтоб было поменьше сюрпризов 🙂
Ну и, как обычно, для себя, чтобы иногда возвращаться, вспоминать и, вероятно, переосмысливать…
Event Loop, однопоточность и первый нюанс
Dart — язык однопоточный. Как же выполняется «параллельный» или асинхронный код в Dart (Flutter)? При запуске приложения выполняется код в методе main и запускается бесконечный цикл Event Loop. Этот цикл проверяет две очереди: microTaskQueue и eventQueue. Если в очереди микротаск появляется задача/задачи, то выполняются они все, потом проверяется очередь событий. И, при появлении события/событий, выполняется первое (потом снова проверяются микротаски). С учетом того, что микротаски в коде Flutter используются меньше 10 раз, мы можем рассматривать только текущий исполняемый код и очередь событий.
При срабатывании какого либо события (нажатие на область экрана, жест, поступление данных из нативки и т.п.), если ему сопоставлен какой-то метод, он ставится в очередь.
Если мы хотим, при выполнении какого-то метода, вынести немного кода в «параллель», мы используем, в первую очередь, Future. Но, возвращаясь на пару абзацев назад, мы должны понимать, что код не будет выполняться одновременно с текущим, а будет помещен в очередь и выполнится потом (асинхронно, а не параллельно). Это в общем случае.
Примеры:
Первый пример (который приводится во многих аналогичных статьях, по моему, ничего не демонстрирует 🙂 Точнее, показывает, но не совсем понятно. Здесь намекается на то, что Future помещает свой метод в очередь и тот выполняется после текущего синхронного кода.
Вот такой пример будет, наверное, нагляднее. Здесь мы видим, что «in example 1» не вписывается нигде в цикле, т.е. не выполняется параллельно, а однозначно дожидается выполнения цикла. И это приводит нас к первому нюансу, который нужно учитывать при программировании на Dart (Flutter).
Здесь мы ожидаем, что функция в Future выполнится через секунду, после того, как мы ее обозначили, но (смотри про Event Loop) с начала выполнится код, расположенный дальше, (каким бы тяжелым он не был) и только потом наш колбек из фьючи(не через 1 секунду, как мы задали, а через две, три или больше). (Если кому-то 10000 тиков цикла мало, поставьте побольше 🙂 )
к содержанию ↑Async await и еще один нюанс
Откладывание кода «в будущее» на столько-то секунд само по себе не столь интересно. Чаще всего мы не знаем, когда этот код точно выполнится, но нам важно, чтобы после его выполнения у нас сработал какой-нибудь колбек. Банальный пример, «дернули» АПИ на беке и, когда нам придет ответ с бэка, мы должны этот ответ как-то обработать. В Dart для этого есть then и cathcError.
Но, когда нам нужно выстраивать цепочку из таких асинхронных функций и прописывать множество then, код становится не очень читаемым. Чтобы этого избежать, в Dart есть async await, по сути, синтаксический сахар для Future().then.
asyncFunc3 помещается в очередь (с учетом нюанса, который опишем ниже) и, когда текущий синхронный код исполнится, выполнится. Там мы дожидаемся выполнения функции mainFunc3 и запускаем колбек с полученным результатом.
Выглядит, на первый взгляд, приятнее и читабельнее. Но, надо заметить, что «await» может кого-то ввести в заблуждение, что код здесь останавливается и ждет… ждет…
На самом деле (нюанс второй) «await» — это, условно, команда «поместить код ниже в очередь, когда придет ответ из асинхронной функции». Но, при условии, что там есть отложенный код (Future).
Какой результат вы ожидаете, если метод и колбек, как ниже? Проверьте себя.
Хм… несколько неожиданно? Или так и предполагали? Мы не остановились ДО await (что логично), мы запустили функцию (а она оказалась синхронной в этом случае) и после получения ответа (сразу же) поместили колбек в очередь.
Таким образом asyncFunc3, описанная выше, аналогичная вот такой:
Ну и вопрос для самопроверки 🙂 Поняли ли вы вышеизложенное. В каком порядке будет меняться extValue?
А ответ у нас будет такой:
Чтобы совсем проникнуться «духом» async/await, поэкспериментируйте с четвертым примером сами. Уберите, хотя бы, await перед Future 🙂
к содержанию ↑И, надеюсь, уловите разницу. Что-то типа:
await — это значит, выполнить код, а по итогу поместить код, который ниже, в очередь; Future(без await) — это поместить код в очередь, а по итогу поместить колбек, если есть, в очередь.
Comleter
Чтобы помочь нам в использовании Future имеется completer, который возвращает фьючу, но это уже другая история… Потому что, с одной стороны, это просто, а с другой не совсем понятно, как и где применять…
к содержанию ↑Isolate и compute
А для работы в разных потоках у нас имеются Isolate и, младший брат, compute. И это точно должна быть отдельная статья.