Как устроена асинхронность в Dart?

Ответ

Асинхронность в Dart построена вокруг Event Loop, Futures и Streams. Это однопоточная модель, которая не блокирует выполнение на операциях ввода-вывода (I/O).

1. Основные абстракции

  • Future<T>: Представляет результат отложенной (асинхронной) операции, которая завершится одним значением (или ошибкой) в будущем.
  • Stream<T>: Представляет последовательность асинхронных событий (данных или ошибок) во времени.

2. Event Loop и Очереди

Dart runtime управляет двумя очередями:

  1. Microtask Queue: Для высокоприоритетных задач (например, завершение Future через completer.complete()).
  2. Event Queue: Для внешних событий (таймеры, I/O, жесты, отрисовка кадров).

Event Loop постоянно работает: сначала выполняет все задачи из Microtask Queue, затем одну задачу из Event Queue, и так по кругу. Это предотвращает «голодание» событий.

3. Практическое использование

С Future и async/await:

Future<String> fetchUserData() async {
  // Имитация сетевого запроса.
  await Future.delayed(Duration(seconds: 1));
  // Имитация декодирования JSON.
  return '{"name": "Alice"}';
}

void main() async {
  print('Fetching data...');
  try {
    final jsonString = await fetchUserData(); // Не блокирует поток.
    final user = jsonDecode(jsonString);
    print('User: ${user['name']}');
  } catch (e) {
    print('Error: $e');
  }
  print('This runs after data is fetched.');
}

Работа со Stream:

Stream<int> timedCounter(Duration interval, int maxCount) async* {
  int count = 0;
  while (count < maxCount) {
    await Future.delayed(interval);
    yield ++count; // «Выдаем» значение в поток.
  }
}

void main() async {
  final stream = timedCounter(Duration(seconds: 1), 5);
  await for (final count in stream) {
    print('Count: $count'); // Печатает 1, 2, 3, 4, 5 с интервалом в секунду.
  }
}

4. Параллелизм: Isolates

Для выполнения CPU-интенсивных задач, чтобы не блокировать Event Loop, используются Isolates. Это отдельные потоки выполнения с собственной памятью, общающиеся через передачу сообщений.

void heavyComputation(SendPort sendPort) {
  // Долгий расчет в отдельном изоляте.
  final result = _calculatePi(100000);
  sendPort.send(result);
}

void main() async {
  final receivePort = ReceivePort();
  await Isolate.spawn(heavyComputation, receivePort.sendPort);
  receivePort.listen((message) {
    print('Result from isolate: $message');
    receivePort.close();
  });
}

Итог: Dart использует неблокирующую асинхронную модель на основе событий, что идеально подходит для I/O-задач в Flutter (сеть, файлы, БД), сохраняя интерфейс плавным.

Ответ 18+ 🔞

А, ну это про асинхронность в Dart! Давай разберём, как эта штука работает, а то многие думают, что там магия какая-то, а на самом деле всё довольно логично, хоть и местами мозг выносит.

Смотри, главная фишка в том, что у тебя один основной поток — Event Loop, который как заведённый хомяк в колесе бегает по кругу. И он нихуя не блокируется на долгих операциях, типа сетевых запросов или чтения файлов. Вместо этого он просто говорит: "Окей, я пока это отложу, а сам пойду другие дела делать".

Основные кирпичики этой конструкции:

  • Future<T> — это типа обещание. "Братан, я тебе значение принесу, но попозже, ок?" Может быть успешным или с ошибкой, но только один раз.
  • Stream<T> — это уже не обещание, а целая лента событий. Как водопроводный кран: открыл — полилось, закрыл — перестало. Значений может быть овердохуища.

А теперь самое вкусное — как это всё крутится.

У нашего хомяка (Event Loop) есть две кормушки, куда ему подкидывают задачи:

  1. Microtask Queue — типа VIP-очередь. Сюда попадают срочные внутренние дела, типа когда ты вручную завершаешь Future. Хомяк сначала ВСЁ выгребает отсюда дочиста.
  2. Event Queue — обычная очередь для всех остальных: таймеры, отклики от сети, клики пользователя. Отсюда хомяк берёт по одной задаче за раз, делает её, и снова смотрит в VIP-очередь.

Вот эта хитрая жопа с приоритетами не даёт интерфейсу виснуть. Пока одна долгая задача из Event Queue ждёт ответа от сервера, хомяк успевает перерисовать экран десять раз и обработать твой тап.

На практике это выглядит как-то так:

С Future и async/await — это просто сахар, чтобы код выглядел почти как синхронный, но без блокировок.

Future<String> fetchUserData() async {
  // Представь, что тут запрос к серверу. Мы его "await'им" — то есть говорим:
  // "Хомяк, иди пока другие дела делай, а как ответ придет — вернись сюда".
  await Future.delayed(Duration(seconds: 1));
  return '{"name": "Alice"}';
}

void main() async {
  print('Начинаю грузить данные...');
  try {
    // Тут программа НЕ ВИСИТ. Хомяк пошёл крутить другие задачи.
    final jsonString = await fetchUserData();
    final user = jsonDecode(jsonString);
    print('Пользователь: ${user['name']}');
  } catch (e) {
    print('Ёпта, ошибка: $e'); // На случай, если сервер лёг.
  }
  print('А это выполнится уже ПОСЛЕ получения данных.');
}

Со Stream — тут уже веселее, можно подписываться на потоки данных.

Stream<int> timedCounter(Duration interval, int maxCount) async* {
  int count = 0;
  while (count < maxCount) {
    await Future.delayed(interval); // Ждём интервал.
    yield ++count; // А вот тут "выплёвываем" значение в поток.
  }
}

void main() async {
  final stream = timedCounter(Duration(seconds: 1), 5);
  // "await for" — это типа "жду каждое новое значение из потока".
  await for (final count in stream) {
    print('Счётчик: $count'); // Напечатает 1, 2, 3, 4, 5 с интервалом в секунду.
  }
}

А если задача такая, что мозг выносит? Например, посчитать число Пи до миллионного знака. Вот тут Event Loop, конечно, охренеет и всё зависнет. Для такого есть Isolates.

Изоляты — это как отдельные квартиры с собственными хомяками и памятью. Они не шастают по твоей памяти, а общаются через почтовые ящики (SendPort/ReceivePort). CPU-интенсивную хуйню нужно пихать туда.

void heavyComputation(SendPort sendPort) {
  // Этот код выполняется в СОВСЕМ ДРУГОМ изоляте, параллельно.
  final result = _calculatePi(100000); // Долгий расчёт.
  sendPort.send(result); // Отправляем результат обратно в основную квартиру.
}

void main() async {
  final receivePort = ReceivePort(); // Наша почтовая ящик.
  // Спавним новый изолят и даём ему адрес ящика.
  await Isolate.spawn(heavyComputation, receivePort.sendPort);

  // Подписываемся на ящик и ждём письмо.
  receivePort.listen((message) {
    print('Результат от изолята: $message');
    receivePort.close(); // Ящик больше не нужен.
  });
}

Итог, ёпта: Dart не блокирует главный поток на I/O операциях, поэтому твой Flutter-интерфейс остаётся плавным. Всё строится на Future (разовое обещание) и Stream (поток событий). А если нужно что-то реально тяжёлое — гоним в отдельный Isolate, чтобы не заставлять нашего хомяка таскать кирпичи.