Асинхронное программирование: futures


Содержание



Что важно:


  • Код в Dart работает в одном треде (прим. thread — поток) выполнения.
  • Из-за кода, который долго занимает (блокирует) тред выполнения, программа может зависнуть.
  • Объекты Future (futures) представляют результаты асинхронных операций — обработки или ввода-вывода, которые будут завершены позже.
  • Чтобы приостановить выполнение до завершения в будущем, используйте await в асинхронной функции (или then() при использовании Future API).
  • Чтобы поймать ошибки, используйте в асинхронной функции конструкцию try-catch (или catchError() при использовании Future API).
  • Для одновременной обработки создайте изолят (или worker для веб-приложения).

Код в Dart работает в одном треде выполнения. Если код занят долгими вычислениями или ожидает операцию I/O, то вся программа приостанавливается.


Асинхронные операции позволяют вашей программе завершить другие задачи в ожидании завершения операции. Dart использует futures для представления результатов асинхронных операций. Для работы с futures можно также использовать async и await или Future API.


Заметка


Весь код выполняется в контексте изолята, которому принадлежит вся память, используемая кодом. В одном и том же изоляте не может быть запущено более одного выполнения кода.

Для параллельного выполнения блоков кода, вы можете выделить их в отдельные изоляты. (Веб-приложения используют workers вместо изолятов.) Обычно каждый из изолятов работает на своем ядре процессора. Изоляты не делят память, и единственный способ, которым они могут взаимодействовать, — это отправка сообщений друг другу. Для погружения в тему смотрите документацию по изолятам или workers.

Введение


Давайте рассмотрим пример кода, который может "заморозить" выполнение программы:


// Synchronous code
void printDailyNewsDigest() {
  var newsDigest = gatherNewsReports(); // Can take a while.
  print(newsDigest);
}

main() {
  printDailyNewsDigest();
  printWinningLotteryNumbers();
  printWeatherForecast();
  printBaseballScore();
}

Наша программа читает из файла новости за день, выводит их, затем выводит еще интересующую пользователя информацию:


<gathered news goes here>
Winning lotto numbers: [23, 63, 87, 26, 2]
Tomorrow's forecast: 70F, sunny.
Baseball score: Red Sox 10, Yankees 0

В данном примере проблема, в том, что все операции после вызова gatherNewsReports()будут ожидать, пока gatherNewsReports() вернет содержимое файла, сколько бы времени это не заняло. Если чтение файла займет много времени, то пользователь будет вынужден ждать в ожидании результатов лотерии, прогноза погоды и победителя недавней игры.


Чтобы поддерживать отзывчивость приложения, авторы Dart используют асинхронную модель при определении функций, выполняющих потенциально дорогостоящую работу. Такие функции возвращают свое значение, используя futures.


Что такое future?


future — экземпляр класса Future<Т>, который представляет собой асинхронную операцию, возвращающую результат типа T. Если результат операции не используются, то тип future указывают Future<void>. При вызове функции, возвращающей future, происходит две вещи:


  1. Функция встает в очередь на выполнение и возвращает незавершенный объект Future.
  2. Позже, когда операция завершена, future завершается со значением или ошибкой.

Для написания кода, зависящего от future, у вас есть два варианта:


  • Использовать asyncawait
  • Использовать Future API

Async — await


Ключевые слова async и await являются частью поддержки асинхронности в Dart. Они позволяют писать асинхронный код, который выглядит как синхронный код и не использует Future API. Асинхронная функция — это функция, перед телом которой находится ключевое слово async. Ключевое слово await работает только в асинхронных функциях.


Примечание: в Dart 1.x, асинхронные функции немедленно откладывают выполнение. В Dart 2 вместо немедленной приостановки асинхронные функции выполняются синхронно до первого await или return.

Следующий код имитирует чтение новостей из файла, используя asyncawait. Откройте DartPad с приложением, запустите и кликните CONSOLE, чтобы увидеть результат.


Код примера
// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';

Future<void> printDailyNewsDigest() async {
  var newsDigest = await gatherNewsReports();
  print(newsDigest);
}

main() {
  printDailyNewsDigest();
  printWinningLotteryNumbers();
  printWeatherForecast();
  printBaseballScore();
}

printWinningLotteryNumbers() {
  print('Winning lotto numbers: [23, 63, 87, 26, 2]');
}

printWeatherForecast() {
  print("Tomorrow's forecast: 70F, sunny.");
}

printBaseballScore() {
  print('Baseball score: Red Sox 10, Yankees 0');
}

const news = '<gathered news goes here>';
const oneSecond = Duration(seconds: 1);

// Imagine that this function is more complex and slow. :)
Future<String> gatherNewsReports() =>
    Future.delayed(oneSecond, () => news);

// Alternatively, you can get news from a server using features
// from either dart:io or dart:html. For example:
//
// import 'dart:html';
//
// Future<String> gatherNewsReportsFromServer() => HttpRequest.getString(
//      'https://www.dartlang.org/f/dailyNewsDigest.txt',
//    );

Обратите внимание, что мы сначала вызываем printDailyNewsDigest(), но новости выводятся последними, даже если файл содержит только одну строку. Это происходит потому, что код, который считывает и печатает файл, выполняется асинхронно.


В этом примере printDailyNewsDigest() выполняет вызов gatherNewsReports(), который является неблокирующим. Вызов метода gatherNewsReports() ставит в очередь работу, но не останавливает выполнение остальной части кода. Программа выводит номера лотереи, прогноз и счет бейсбольного матча; программа печатает новости после завершения их сбора gatherNewsReports(). Если gatherNewsReports() занимает некоторое время для завершения своей работы, ничего страшного не происходит: пользователь может читать другие вещи до того, как будет напечатан ежедневный дайджест новостей.


Обратите внимание на возвращаемые типы. Возвращаемым типом функции gatherNewsReports() является Future<String>, что означает, что она возвращает future, которое завершается строковым значением. Функция printDailyNewsDigest(), которая не возвращает значение, имеет возвращаемый тип Future<void>.


На следующей диаграмме показаны шаги выполнения кода.



  1. Начинается выполнение приложения.
  2. Функция main() вызывается асинхронную функцию printDailyNewsDigest(), которое начинается выполняться синхронно.
  3. printDailyNewsDigest() использует await для вызова функции gatherNewsReports(), которая начинает выполняться.
  4. gatherNewsReports() возвращает незавершенное future (экземпляр Future<String>).
  5. Поскольку printDailyNewsDigest() является асинхронной функцией и ожидает значение, то она приостанавливает выполнение и возвращает вызывающей функции main () незавершенное future (в данном случае экземпляр Future<void>).
  6. Выполняются остальные функции вывода. Так как они синхронные, то каждая функция выполняется полностью перед переходом к следующей. Например, все выигрышные номера лотереи выведутся до прогноза погоды.
  7. После завершения выполнения main() асинхронные функции могут возобновить выполнение. Сначала получаем future с новостями по завершению gatherNewsReports(). Затем printDailyNewsDigest() продолжает выполнение, выводя новости.
  8. По окончанию выполнения printDailyNewsDigest() завершается первоначально полученное future и происходит выход из приложения.

Обратите внимание, что асинхронная функция начинает выполняться сразу (синхронно). Функция приостанавливает выполнение и возвращает незавершенное future при достижении первого появления любого из следующих действий:


  • Первое выражение await (после того, как функция получает незавершенное future из этого выражения).
  • Любой оператор return в функции.
  • Конец тела функции.

Обработка ошибок


Скорее всего вы хотели бы "отловить" ошибку выполнения функции, возвращающей future. В асинхронных функциях можно обрабатывать ошибки с помощью try-catch:


Future<void> printDailyNewsDigest() async {
  try {
    var newsDigest = await gatherNewsReports();
    print(newsDigest);
  } catch (e) {
    // Handle error...
  }
}

Блок try-catch с асинхронным кодом ведет себя так же, как и с синхронным кодом: если код в блоке try создает исключение, выполняется код внутри catch.


Последовательное выполнение


Можно использовать несколько выражений await, чтобы убедиться, что каждая инструкция завершается перед выполнением следующей:


// Sequential processing using async and await.
main() async {
  await expensiveA();
  await expensiveB();
  doSomethingWith(await expensiveC());
}

expensiveB() функция не выполняется, пока expensiveA() завершится, и так далее.


Future API


До того, как async и await были добавлены в Dart 1.9, вы должны были использовать Future API. Вы и сейчас можете встретить использование Future API в старом коде и в коде, который нуждается в большей функциональности, чем async–await может предложить.


Чтобы написать асинхронный код с помощью Future API, используйте метод then() для регистрации обратного вызова. Этот обратный вызов сработает, когда future завершится.


Следующий код имитирует чтение новостей из файла, используя Future API. Откройте DartPad с приложением, запустите и кликните CONSOLE, чтобы увидеть результат.


Код примера
// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';

Future<void> printDailyNewsDigest() {
  final future = gatherNewsReports();
  return future.then(print);
  // You don't *have* to return the future here.
  // But if you don't, callers can't await it.
}

main() {
  printDailyNewsDigest();
  printWinningLotteryNumbers();
  printWeatherForecast();
  printBaseballScore();
}

printWinningLotteryNumbers() {
  print('Winning lotto numbers: [23, 63, 87, 26, 2]');
}

printWeatherForecast() {
  print("Tomorrow's forecast: 70F, sunny.");
}

printBaseballScore() {
  print('Baseball score: Red Sox 10, Yankees 0');
}

const news = '<gathered news goes here>';
const oneSecond = Duration(seconds: 1);

// Imagine that this function is more complex and slow. :)
Future<String> gatherNewsReports() =>
    Future.delayed(oneSecond, () => news);

// Alternatively, you can get news from a server using features
// from either dart:io or dart:html. For example:
//
// import 'dart:html';
//
// Future<String> gatherNewsReportsFromServer() => HttpRequest.getString(
//      'https://www.dartlang.org/f/dailyNewsDigest.txt',
//    );

Обратите внимание, что мы сначала вызываем printDailyNewsDigest(), но новости выводятся последними, даже если файл содержит только одну строку. Это происходит потому, что код, который считывает и печатает файл, выполняется асинхронно.


Это приложение выполняется следующим образом:


  1. Начинается выполнение приложения.
  2. Основная функция вызывает функцию printDailyNewsDigest(), которая не возвращает результат сразу, а сначала вызывает gatherNewsReports().
  3. gatherNewsReports() начинает читать новости и возвращает future.
  4. В printDailyNewsDigest() используется then() для регистрации колбэка, который примет в качестве параметра значение, полученное по завершению future. Вызов then() возвращает новое future, которая завершится значением, возвращенным колбэком из then().
  5. Выполняются остальные функции вывода. Так как они синхронные, то каждая функция выполняется полностью перед переходом к следующей. Например, все выигрышные номера лотереи выведутся до прогноза погоды.
  6. Когда все новости получены, future, возвращаемое функцией gatherNewsReports(), завершается строкой, содержащей собранные новости.
  7. Код, указанный в then() в printDailyNewsDigest(), выполняется, выводя новости.
  8. Приложение завершает работу.

Примечание: в функции printDailyNewsDigest() код future.then(print) эквивалентен следующему: future.then((newsDigest) => print(newsDigest)).

Также код внутри then() может использовать фигурные скобки:


Future<void> printDailyNewsDigest() {
  final future = gatherNewsReports();
  return future.then((newsDigest) {
    print(newsDigest);
    // Do something else...
  });
}

Необходимо указать аргумент колбэка в then(), даже если future имеет тип Future<void>. По соглашению неиспользуемый аргумент определяется через _ (подчеркивание).


final future = printDailyNewsDigest();
return future.then((_) {
  // Code that doesn't use the `_` parameter...
  print('All reports printed.');
});

Обработка ошибок


Используя Future API, можно отловить ошибку с помощью catchError():


Future<void> printDailyNewsDigest() =>
    gatherNewsReports().then(print).catchError(handleError);

Если новости недоступны для чтения, то код выше выполняется следующим образом:


  1. future, возвращаемое gatherNewsReports(), завершается с ошибкой.
  2. future, возвращаемое then(), завершается с ошибкой, print() не вызывается.
  3. Колбэк в catchError() (handleError()) отлавливает ошибку, future, возвращаемое catchError(), завершается нормально, и ошибка дальше не распространяется.

Цепочка then()catchError() является распространенным шаблоном при использовании Future API. Рассматривайте эту пару, как эквивалент блока try-catch в Future API.

Как и then(), catchError() возвращает новую future, которое завершается возвращаемым значением колбэка. Для погружения в тему читайте Futures and Error Handling.


Вызов нескольких функций, возвращающих future


Рассмотрим три функции: expensiveA(), expensiveB(),expensiveC(), — которые возвращают future. Вы можете вызывать их последовательно (одна функция запускается после завершения предыдущей), или вы можете запустить их все одновременно и сделать что-то, как только все значения вернутся. Интерфейс Future достаточно гибок, чтобы реализовать оба варианта использования.


Цепочка вызовов функций с помощью then()
Когда функции, возвращающие future, должны выполняться по порядку, используйте "цепочку" из then():


expensiveA()
    .then((aValue) => expensiveB())
    .then((bValue) => expensiveC())
    .then((cValue) => doSomethingWith(cValue));

Вложение колбэков тоже работает, но его сложнее читать. (прим. http://callbackhell.com/)


Ожидание завершения нескольких futures с помощью Future.wait()
Если порядок выполнения функций не важен, можно использовать Future.wait(). Когда вы для функции Future.wait() указываете список futures, как параметры, она сразу возвращает future. Эта future не завершится, пока не завершатся все указанные futures. Данная future завершится списком результатов всех указанных futures.


Future.wait([expensiveA(), expensiveB(), expensiveC()])
    .then((List responses) => chooseBestResponse(responses, moreInfo))
    .catchError(handleError);

Если вызов любой из функций завершается ошибкой, то и future, возвращаемая Future.wait(), также завершается ошибкой. Используйте catchError(), чтобы отловить эту ошибку.




Что еще почитать?


Dart 2. Асинхронное программирование: потоки данных

Комментарии (0)