1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/CarGuo-GSYFlutterBook

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
Flutter-N44.md 18 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 10.03.2025 00:06 5767d61

Небольшие хитрости Flutter: интересные асинхронные вопросы в вопросах на собеседование

Долго не обновлял серию небольших хитростей Flutter, поэтому решил рассказать о некоторых базовых знаниях о асинхронной работе, которые могут встретиться вам при прохождении собеседования.

Сначала давайте рассмотрим простой пример кода, представленный ниже:

  • testFunc выполняет циклическую задачу каждые 1 секунду с помощью asyncWork.
  • Внутри asyncWork, перед выполнением основной работы проверяется наличие данных в списке list.
  • Если данные присутствуют, производится некоторое предварительное действие (симулирование выполнения через задержку в 2 секунды).
  • После этого происходит удаление первого элемента списка с использованием метода 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.

image

При выполнении обратного вызова, как показано ниже, callback(timer) является обычным вызовом, который не использует await или другие операторы ожидания. Таким образом, для Timer.periodic не важно, является ли asyncWork объектом типа Future, или был ли он уже выполнен; его основная цель — просто выполнить эту работу и перейти к следующему шагу. В результате этого возникают проблемы с логическими проверками в начале кода: многократное выполнение и ожидание.

image

Кроме того, помните, что мы говорили ранее, Timer.periodic выполняется с периодичностью примерно каждую секунду, почему здесь используется слово "примерно"? Потому что Timer.periodic не является надёжным временщиком. Это указано в официальной документации:

Точные временные метки зависят от реализации внутреннего таймера. Не более n обратных вызовов будет сделано за время duration * n, но время между двумя последовательными обратными вызовами может быть меньше или больше чем duration.image

На самом деле причины просты: для виртуальной машины (VM) управление таймерами осуществляется путём "пакетной обработки", и различные среды выполнения могут иметь различную производительность. Он гарантирует, что "не более n обратных вызовов будет сделано за время duration * n", но не гарантирует "времени между двумя последовательными обратными вызовами". Соответственно, конечное время выполнения может быть меньше или больше duration. Поэтому вы установили период Timer в 50 миллисекунд, но время выполнения может колебаться от 40 до 500 миллисекунд. Иногда несколько таймеров могут привести к тому, что интервал выполнения будет "внезапно" достигать 500 миллисекунд.

Для задач, требующих более точной синхронизации, можно использовать:

  • Ticker, так как Flutter использует 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, то...

  • MicroTask используется для выполнения операций, которые требуются в реальном времени и имеют высокое значение; при этом следует стремиться к тому, чтобы очередь MicroTask была как можно короче.
  • Event используется для обработки обычных асинхронных событий или взаимодействия пользователя с приложением; например, когда пользователь взаимодействует с приложением, он может создать событие нажатия кнопки и добавить его в очередь Event; цикл событий Dart выполняет код обработки события, связанного с этим нажатием.Чтобы использовать MicroTask, мы можем сделать так, чтобы приложение более точно выполняло задачи, не нагружая тем самым очередь Event до такой степени, что интерфейс становится не откликающимся. Например, можно использовать MicroTask для асинхронной обработки данных JSON и преобразования их в объекты, что поможет предотвратить замедление работы интерфейса.

👆 Без учёта создания нового изолята.

Рассмотрим пример, где мы модифицировали 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)` выполняется после завершения всех операций в рамках будущей задачи, чтобы гарантировать её полное выполнение.
- После завершения всех будущих задач первого уровня запускается отложенный вызов, который затем выполняет вторую будущую задачу. Поскольку эта вторая будущая задача возвращает ещё одну будущую задачу, она будет выполнена последней.

> ⚠️ Время выполнения отложенного вызова может варьироваться; он может быть выполнен в конце.

![Изображение](http://img.cdn.guoshuyu.cn/20240618_N44/image8.png)
```Как видно из приведенного примера, весь процесс асинхронной работы строится вокруг микротасков, но при этом гарантируется выполнение всех будущих задач до выполнения следующего шага.> К тому же, в некоторых старых версиях Dart может произойти выполнение `микротаски #0 (из будущей задачи #2b)` только после завершения всех первичных будущих задач.

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

![](http://img.cdn.guoshuyu.cn/20240618_N44/image9.png)

**Поэтому использование `Timer` как точки входа для понимания асинхронной работы представляет собой очень интересный подход. Особенно начальные примеры помогут лучше понять механизм работы асинхронных задач и таймеров. Дальнейшие детали микротасков также помогут глубже понять работу асинхронных задач в Flutter, что делает этот материал отличным выбором для проведения собеседования**.

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/CarGuo-GSYFlutterBook.git
git@api.gitlife.ru:oschina-mirror/CarGuo-GSYFlutterBook.git
oschina-mirror
CarGuo-GSYFlutterBook
CarGuo-GSYFlutterBook
master