Долго не обновлял серию небольших хитростей Flutter, поэтому решил рассказать о некоторых базовых знаниях о асинхронной работе, которые могут встретиться вам при прохождении собеседования.
Сначала давайте рассмотрим простой пример кода, представленный ниже:
testFunc
выполняет циклическую задачу каждые 1 секунду с помощью asyncWork
.asyncWork
, перед выполнением основной работы проверяется наличие данных в списке list
.removeAt
.List<String> list = ["1"];
void testFunc() {
Timer.periodic(const Duration(seconds: 1), asyncWork);
}
Future<void> asyncWork(Timer t) async {
if (list.isNotEmpty) {
// Выполнение какого-то действия
await Future.delayed(const Duration(seconds: 2));
String item = list.removeAt(0);
if (kDebugMode) {
print("############ complete $item ############");
}
}
}
Так вот, есть ли какие-либо проблемы в этом коде? На самом деле, этот код может выдать ошибку во время выполнения, причём ошибка будет связана с тем, что список list
становится пустым, но мы всё ещё вызываем removeAt(0)
.Это довольно "странно", так как мы уже проверяем наличие элементов в начале каждого выполнения asyncWork
. Почему же тогда список оказывается пустым при попытке удаления?
Здесь важно понять, что когда мы используем await
для задержки на 2 секунды, это позволяет следующему вызову asyncWork
начаться раньше, чем завершается текущий вызов. Таким образом, даже если мы проверили, что список не пуст, за время задержки другой вызов мог уже удалить все элементы из списка.
Почему стоит говорить об этом? Это потому что здесь мы рассматриваем ситуацию, где задержка фиксирована, и её легко диагностировать. Однако, если бы логика была более сложной, и различные машины обрабатывали запросы с разной скоростью, то такие асинхронные проблемы становились бы намного труднее для диагностики.> Теперь вы понимаете, почему, хотя мы проверили с помощью isNotEmpty
, метод removeAt
всё равно может выбросить ошибку RangeError(index)
?
Некоторые могут удивиться, что Dart — это однопоточная система, которая работает в цикле событий. Почему же после использования await
несколько потоков могут одновременно войти в этот участок кода?На самом деле, именно потому, что Dart использует однопоточную систему событий, происходит такая ситуация, когда несколько потоков могут одновременно входить в этот участок кода. Поэтому важно понять механизм работы Timer
. **Основной временной механизм Timer
реализован с использованием изолированных потоков (isolates
).**Мы знаем, что в Flutter Dart использует однопоточную систему событий, но мы можем использовать изолируемые потоки для запуска новых асинхронных задач. Для Dart VM это реализуется через порты изолируемых потоков, поэтому в случае таймеров используется изолируемый поток, который затем вызывает выполнение обратного вызова через SendPort
.
При выполнении обратного вызова, как показано ниже, callback(timer)
является обычным вызовом, который не использует await
или другие операторы ожидания. Таким образом, для Timer.periodic
не важно, является ли asyncWork
объектом типа Future
, или был ли он уже выполнен; его основная цель — просто выполнить эту работу и перейти к следующему шагу. В результате этого возникают проблемы с логическими проверками в начале кода: многократное выполнение и ожидание.
Кроме того, помните, что мы говорили ранее, Timer.periodic
выполняется с периодичностью примерно каждую секунду, почему здесь используется слово "примерно"? Потому что Timer.periodic
не является надёжным временщиком. Это указано в официальной документации:
Точные временные метки зависят от реализации внутреннего таймера. Не более
n
обратных вызовов будет сделано за времяduration * n
, но время между двумя последовательными обратными вызовами может быть меньше или больше чемduration
.
На самом деле причины просты: для виртуальной машины (VM) управление таймерами осуществляется путём "пакетной обработки", и различные среды выполнения могут иметь различную производительность. Он гарантирует, что "не более n
обратных вызовов будет сделано за время duration * n
", но не гарантирует "времени между двумя последовательными обратными вызовами". Соответственно, конечное время выполнения может быть меньше или больше duration
. Поэтому вы установили период Timer
в 50 миллисекунд, но время выполнения может колебаться от 40 до 500 миллисекунд. Иногда несколько таймеров могут привести к тому, что интервал выполнения будет "внезапно" достигать 500 миллисекунд.
Для задач, требующих более точной синхронизации, можно использовать:
ticker
для рендеринга экрана со скоростью 60 кадров в секунду. Поэтому Ticker
можно рассматривать как специальный циклический таймер.Timer.periodic
, например, запустив новый isolate
и использовав microseconds: 500
для запуска Timer
. Внутри обратного вызова можно самостоятельно контролировать частоту тиков (tickRate
) для триггерирования событий таймера. reliable_periodic_timer реализован именно таким образом.Используя этот пример с Timer
, давайте поговорим ещё немного об асинхронной модели в Flutter, ранее мы говорили, что по умолчанию Dart работает с однопоточной моделью запрос-ответ. Как это работает?Проще говоря, но не совсем точно: Dart-runtime Root isolate представляет собой бесконечный цикл потока, который выполняет Dart-код. При встрече с await Future
(или async
), Dart-цикл событий может завершить выполнение других Dart-операций перед тем, как вернуться к выполнению следующего шага после завершения Future
.Это объясняет, почему
asyncWork
многократно заходит внутрь себя, каждый раз проверяяlist.isNotEmpty
. Так как выполнение действия требуетawait
на 2 секунды, то каждое выполнение пропускаетlist.removeAt
, что в конечном итоге приводит к ошибкеRangeError(index)
при попытке удаления элемента.
asyncWork(t) async {
if (list.isNotEmpty) {
/// Выполнение некоторого действия
await Future.delayed(Duration(seconds: 2));
var item = list.removeAt(0);
if (kDebugMode) {
print("############ complete $item ############");
}
}
}
Однако в Dart есть различные типы асинхронных операций, которые можно разделить на микротаски (MicroTasks) и события (Events).
В цикле событий Dart сначала обрабатывает очередь микротасков, если она пуста, то управление переходит к очереди событий. Эта механика гарантирует, что микротаски будут обработаны раньше, чем события пользователя.
Что касается Flutter, то...
👆 Без учёта создания нового изолята.
Рассмотрим пример, где мы модифицировали asyncWork
, как показано ниже. Здесь выполняется один Future
и один Future.microtask
. Какой будет результат выполнения?
asyncWork(t) async {
print('начинается');
Future(() => print('Это новый Future'));
Future.microtask(() => print('Это микротаск'));
print('заканчивается');
}
Как видно на следующем рисунке, хотя microtask
был добавлен позже, поскольку это MicroTask, он имеет преимущество перед Future
и выполняется первым, хотя Future
и Future.microtask
выполняются последовательно, но существует приоритетность вызова обратного вызова.
Давайте рассмотрим более сложный пример, представленный ниже. Теперь здесь приведены различные вложения Futures и MicroTasks:
asyncWork(t) async {
print('основной #1 из 2');
scheduleMicrotask(() => print('микротаск #1 из 3'));
Future.delayed(Duration(seconds: 1), () => print('будущее #1 (задержка)'));
Future(() => print('будущее #2 из 4'))
.then((_) => print('будущее #2a'))
.then((_) {
print('будущее #2b');
scheduleMicrotask(() => print('микротаск #0 (из будущего #2b)'));
}).then((_) => print('будущее #2c'));
}
``````markdown
## Из результатов можно сделать вывод:
- Выводы функции `main` были выполнены первыми, что логично.
- Затем выполняются три микротаска первого уровня, так как они имеют приоритет.
- После этого начинают выполняться будущие задачи первого уровня, начиная с `будущей задачи #2 из 4`, а также её последующий вывод, поскольку они относятся к одной будущей задаче. Интересной особенностью является выполнение `микротаска #0 (из будущей задачи #2b)`.
- Обратите внимание, что `микротаск #0 (из будущей задачи #2b)` выполняется после завершения всех операций в рамках будущей задачи, чтобы гарантировать её полное выполнение.
- После завершения всех будущих задач первого уровня запускается отложенный вызов, который затем выполняет вторую будущую задачу. Поскольку эта вторая будущая задача возвращает ещё одну будущую задачу, она будет выполнена последней.
> ⚠️ Время выполнения отложенного вызова может варьироваться; он может быть выполнен в конце.

```Как видно из приведенного примера, весь процесс асинхронной работы строится вокруг микротасков, но при этом гарантируется выполнение всех будущих задач до выполнения следующего шага.> К тому же, в некоторых старых версиях Dart может произойти выполнение `микротаски #0 (из будущей задачи #2b)` только после завершения всех первичных будущих задач.
Наконец, стоит отметить интересный факт: конструктор `Future()`, а также метод `Future.delayed` реализуются через объект `Timer`. Это довольно любопытно, так как все сводится к работе с таймерами. Поэтому когда множество `Future.delayed` или `Future()` создает коллбэки, это фактически сводится к одному механизму «пакетного» выполнения через `Timer`.

**Поэтому использование `Timer` как точки входа для понимания асинхронной работы представляет собой очень интересный подход. Особенно начальные примеры помогут лучше понять механизм работы асинхронных задач и таймеров. Дальнейшие детали микротасков также помогут глубже понять работу асинхронных задач в Flutter, что делает этот материал отличным выбором для проведения собеседования**.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )