13. Асинхронные функции и Future
Изучите роль отложенного выполнения Future, await и block_on.
Wave Foundation
Выражение ожидающих задач
Для операций ожидания, таких как файлы, сокеты и таймеры, необходимо различать продолжающиеся вычисления и ожидание завершения. Асинхронная функция выражает результат, который необходимо завершить, как Future. Добавление async не создает автоматически новый поток и не меняет все синхронные вызовы на асинхронные.
Прочтите эту главу после «Функции, указатели и обработка ошибок». Примером является собственная программа, которая использует средство запуска std::task и запускает каждый файл как wavec run main.wave.
Создайте Future и получите результаты
import("std::task" as task);
async fun calculate(value: i64) -> i64 {
return value * 2;
}
fun main() {
var pending: Future<i64> = calculate(21);
var result: i64 = task::block_on(pending);
task::shutdown();
println("{}", result);
}
Результат выполнения:
42
i64, записанный в объявлении calculate, представляет собой значение, полученное после завершения. Результатом самого вызова является Future. В обычном main запустите Future с block_on и получите готовый результат.
Вызов завершения работы после очистки всех задач для освобождения ресурсов исполнителя. Не освобождайте буфер, пока задача его еще использует, и не игнорируйте незавершенные задачи.
Вызов и выполнение тела различны
Асинхронные функции выполняются лениво. Ее следует отличать от обычной функции, которая выполняет свое тело сразу после ее вызова.
import("std::task" as task);
static entered: i32 = 0;
async fun work() -> i32 {
entered += 1;
return 7;
}
fun main() {
var pending: Future<i32> = work();
println("before={}", entered);
var result: i32 = task::block_on(pending);
task::shutdown();
println("after={} result={}", entered, result);
}
Результат выполнения:
before=0
after=1 result=7
Когда Future был создан, entered все еще был 0. После запуска выполнения тело выполняется и становится 1. Вот почему вы не должны рассматривать задачу как завершенную только потому, что Future хранится в переменной.
Ожидание внутри асинхронной функции
Внутри функции async await ожидает завершения другой Future. Результатом выражения await является значение завершения.
import("std::task" as task);
async fun twice(value: i32) -> i32 {
await task::yield_now();
return value * 2;
}
async fun process() -> i32 {
var value: i32 = await twice(20);
return value + 2;
}
fun main() {
var answer: i32 = task::block_on(process());
task::shutdown();
println("{}", answer);
}
Результат выполнения:
42
process ожидает Future из twice, а затем добавляет 2 к значению завершения. Вы можете использовать значение, аналогичное результату вызова обычной функции, но во время ожидания передать возможность выполнения другой задаче.
yield_now предоставляет возможности совместного выполнения. Невыполнение команды даже один раз в длинном цикле вычислений может замедлить выполнение других задач. Асинхронный CPU не является устройством для автоматического распараллеливания и распределения вычислений.
Запланируйте несколько задач
Вы можете планировать задачи с помощью spawn и ждать соответствующих результатов. Это пример проверки конечного результата без использования промежуточного порядка вывода двух операций.
import("std::task" as task);
async fun compute(value: i32) -> i32 {
await task::yield_now();
return value * 2;
}
async fun combine() -> i32 {
var first: Future<i32> = task::spawn(compute(10));
var second: Future<i32> = task::spawn(compute(20));
var left: i32 = await first;
var right: i32 = await second;
return left + right;
}
fun main() {
var total: i32 = task::block_on(combine());
task::shutdown();
println("{}", total);
}
Результат выполнения:
60
У каждой задачи, которую вы планируете, есть место, где она ожидает результатов. Вместо того, чтобы создавать задачу и забывать ее хэндл, вам нужно решить, кто будет проверять ее выполнение. await Не считайте последовательность и порядок выполнения внутренних операций одинаковыми.
Употребите Future один раз.
Future рассматривается как одиночный дескриптор потребления. Копирование одного и того же Future не заставляет его ждать, как две разные задачи. Не повторяйте block_on или await для Future, который уже завершен.
Если вам нужен один и тот же результат в нескольких местах, вместо многократного использования Future сохраните завершенное значение и передайте его в соответствии с правилами копирования и обмена для этого значения. Вам также следует проверить, есть ли в значении указатели или собственные ресурсы.
Разница между таймером и синхронным ожиданием
async При ожидании внутри функции вы можете использовать await task::sleep_ms(...). Вызов синхронного sleep блокирует текущий поток выполнения, что также может повлиять на ход выполнения других задач в исполнителе.
Я не ожидаю, что время ожидания будет в точности равным запрошенному количеству миллисекунд. В зависимости от вашего расписания и других задач вы можете проснуться поздно. При реализации таймаутов мы используем clock и deadline для измерения затраченного времени, а не ожидания каждый раз снова исходного полного времени.
Время жизни буфера и его отмена
Буфер, переданный в асинхронный ввод-вывод, должен оставаться действительным, даже если вызывающая функция приостановлена. Освобождение или перераспределение его до завершения или отмены очистки может привести к тому, что операция будет иметь недопустимый адрес.
Нельзя предполагать, что запрос на отмену и завершение задачи происходят одновременно. Проверьте результаты серии cancel API и дождитесь необходимого завершения, прежде чем освобождать ресурсы. Пожалуйста, прочтите См. task. для получения подробных правил звонков.
обычное недоразумение
| думать | на самом деле проверь |
|---|---|
| async Я позвонил, и все кончено. | Вы действительно запустили и выполнили Future? |
| async Все вызовы внутри функции асинхронны. | Вызываемый API синхронный или асинхронный? |
| Future Копирование дублирует задачу. | Вы постоянно используете один и тот же дескриптор? |
| Поскольку вы отменили его, вы можете немедленно освободить буфер. | Вы закончили организацию работы после отмены? |
| Промежуточный порядок вывода всегда фиксирован. | Ожидает ли он только того порядка, который необходим для результата? |
Упражнение и полное решение
Создайте конвейер, который ожидает три асинхронные функции подряд. Возвращает результат удвоения и прибавления 5.
import("std::task" as task);
async fun read_value() -> i32 {
return 10;
}
async fun transform(value: i32) -> i32 {
await task::yield_now();
return value * 2;
}
async fun pipeline() -> i32 {
var initial: i32 = await read_value();
var changed: i32 = await transform(initial);
return changed + 5;
}
fun main() {
var result: i32 = task::block_on(pipeline());
task::shutdown();
println("{}", result);
}
Результат выполнения:
25
Этот пример намеренно представляет собой последовательную зависимость. transform требует результата read_value, поэтому простое выполнение всех spawn не приведет к исчезновению связи. Отправной точкой асинхронного проектирования является различие между независимыми операциями и операциями, требующими результатов.
После того, как вы освоили основы, подключитесь к реальным внешним ресурсам с помощью Практика чтения файлов и TCP Практика.
void и never
Обычная функция, в которой отсутствует тип возвращаемого значения, может вернуться в точку вызова без значения. Тип never записывается как !, что означает, что он не возвращается в точку вызова в обычном режиме. Характерным примером является функция завершения процесса.
Пример, иллюстрирующий объявление:
fun log(message: str) // 작업 후 돌아옴, 결과값 없음
fun stop(code: i32) -> ! // 정상 반환하지 않음
async fun work() -> i64 // 호출 결과는 Future<i64>
Попыток сделать never общим сохраненным значением не предпринимается. Не пишите функции, объявленные как невозвратные, чтобы иметь нормальный путь возврата. Если перед выходом требуется очистка ресурса, сначала это должен сделать вызывающий объект.
Полный пример функции, которая не возвращает значение
Если вы сохраните его как main.wave и запустите, он закончится кодом выхода 0 без вывода. stop не возвращается вызывающей стороне, поэтому он объявляется как -> !.
import("std::process::core")::{
proc_exit
};
fun stop() -> ! {
proc_exit(0);
}
fun main() {
stop();
}