Асинхронное программирование: 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
, происходит две вещи:
- Функция встает в очередь на выполнение и возвращает незавершенный объект
Future
. - Позже, когда операция завершена,
future
завершается со значением или ошибкой.
Для написания кода, зависящего от future
, у вас есть два варианта:
- Использовать
async
—await
- Использовать
Future
API
Async — await
Ключевые слова async
и await
являются частью поддержки асинхронности в Dart. Они позволяют писать асинхронный код, который выглядит как синхронный код и не использует Future
API. Асинхронная функция — это функция, перед телом которой находится ключевое слово async
. Ключевое слово await
работает только в асинхронных функциях.
Примечание: в Dart 1.x, асинхронные функции немедленно откладывают выполнение. В Dart 2 вместо немедленной приостановки асинхронные функции выполняются синхронно до первогоawait
илиreturn
.
Следующий код имитирует чтение новостей из файла, используя async
— await
. Откройте 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>
.
На следующей диаграмме показаны шаги выполнения кода.
- Начинается выполнение приложения.
- Функция
main()
вызывается асинхронную функциюprintDailyNewsDigest()
, которое начинается выполняться синхронно. printDailyNewsDigest()
используетawait
для вызова функцииgatherNewsReports()
, которая начинает выполняться.gatherNewsReports()
возвращает незавершенноеfuture
(экземплярFuture<String>
).- Поскольку
printDailyNewsDigest()
является асинхронной функцией и ожидает значение, то она приостанавливает выполнение и возвращает вызывающей функцииmain ()
незавершенноеfuture
(в данном случае экземплярFuture<void>
). - Выполняются остальные функции вывода. Так как они синхронные, то каждая функция выполняется полностью перед переходом к следующей. Например, все выигрышные номера лотереи выведутся до прогноза погоды.
- После завершения выполнения
main()
асинхронные функции могут возобновить выполнение. Сначала получаемfuture
с новостями по завершениюgatherNewsReports()
. ЗатемprintDailyNewsDigest()
продолжает выполнение, выводя новости. - По окончанию выполнения
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()
, но новости выводятся последними, даже если файл содержит только одну строку. Это происходит потому, что код, который считывает и печатает файл, выполняется асинхронно.
Это приложение выполняется следующим образом:
- Начинается выполнение приложения.
- Основная функция вызывает функцию
printDailyNewsDigest()
, которая не возвращает результат сразу, а сначала вызываетgatherNewsReports()
. gatherNewsReports()
начинает читать новости и возвращаетfuture
.- В
printDailyNewsDigest()
используетсяthen()
для регистрации колбэка, который примет в качестве параметра значение, полученное по завершениюfuture
. Вызовthen()
возвращает новоеfuture
, которая завершится значением, возвращенным колбэком изthen()
. - Выполняются остальные функции вывода. Так как они синхронные, то каждая функция выполняется полностью перед переходом к следующей. Например, все выигрышные номера лотереи выведутся до прогноза погоды.
- Когда все новости получены,
future
, возвращаемое функциейgatherNewsReports()
, завершается строкой, содержащей собранные новости. - Код, указанный в
then()
вprintDailyNewsDigest()
, выполняется, выводя новости. - Приложение завершает работу.
Примечание: в функции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);
Если новости недоступны для чтения, то код выше выполняется следующим образом:
future
, возвращаемоеgatherNewsReports()
, завершается с ошибкой.future
, возвращаемоеthen()
, завершается с ошибкой,print()
не вызывается.- Колбэк в
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()
, чтобы отловить эту ошибку.