Async/await
Async/await — это синтаксическая конструкция, реализуемая во многих языках программирования, которая позволяет создавать асинхронные, неблокирующие функции с использованием структуры, напоминающей обычные синхронные функции. Семантически и технически async/await связаны с концепцией корутин и зачастую реализуются схожими средствами. Основная цель использования такого подхода заключается в том, чтобы выполнять иной код программы во время ожидания завершения длительных асинхронных операций, обычно представляемых через промисы, фьючерсы или похожие структуры данных[1]. Подобная функциональность реализована в языках C#[1], C++, Python, F#, Hack, Julia, Dart, Kotlin, Rust[2], Nim[3], JavaScript (стандарт ECMAScript 2017) и Swift[4].
История
В 2007 году F# получил поддержку асинхронных рабочих процессов (asynchronous workflows) с точками ожидания (await points) в версии 2.0[5]. Эта функциональность была впервые представлена в виде отдельно устанавливаемого дополнения (Community Technology Preview) для Visual Studio 2008[6], а полная интеграция в среду разработки произошла с выходом Visual Studio 2010[7]. Эта реализация повлияла на появление механизма async/await в C#[8].
Компания Microsoft впервые реализовала поддержку async/await в экспериментальной версии C# Async CTP (2011). Официально эта функциональность стала частью языка с выпуском C# 5 (2012)[9][1].
Основной разработчик Haskell Саймон Марлоу (Simon Marlow) создал пакет async в 2012 году[10].
Поддержка async/await появилась в Python с выходом версии 3.5 (2015 год), где были добавлены два новых ключевых слова: async и await[11].
В TypeScript аналогичная поддержка реализована с версии 1.7 (2015)[12]. Ключевым развитием стал выход TypeScript 2.1 (2016), в котором появилась возможность компиляции `async/await` для более старых версий JavaScript (ES3 и ES5), что сделало эту конструкцию доступной для использования в большинстве браузеров при наличии полифилла для `Promise`[13][14].
В JavaScript конструкция async/await появилась в стандарте ECMAScript 2017 (2017 год).
В Perl поддержка синтаксиса `async/await` реализуется через модули из репозитория CPAN. Ключевым является модуль `Future::AsyncAwait`, активная разработка которого велась в 2019 году Полом Эвансом[15]. Он предоставляет синтаксический сахар в виде ключевых слов `async sub` и `await` для работы с объектами `Future`, которые представляют отложенные вычисления, и используется в связке с фреймворками для асинхронного ввода-вывода, такими как `IO::Async`[16][17].
В Rust поддержка async/await появилась в версии 1.39.0 (2019 год) с использованием ключевого слова async и постфиксного оператора .await, введённых в редакции языка 2018 года[18].
В C++ официальная поддержка async/await реализована в стандарте C++20, где появились новые ключевые слова co_return, co_await и co_yield (2020 год).
Язык программирования Swift реализовал поддержку async/await с версии 5.5 (2021), добавив ключевые слова async и await. Эта версия также включает реализацию акторной модели с помощью ключевого слова actor, где для организации доступа к каждому актору используется async/await[19].
В 2025 году язык программирования Zig представил новую модель асинхронного ввода-вывода. Вместо синтаксиса `async/await` в традиционном виде, язык предлагает разделение вызова функции и возврата из неё, что позволяет выполнять несколько операций одновременно[20].
Новейшие разработки (2024—2025)
В 2024—2025 годах развитие асинхронного программирования было сосредоточено на улучшении эргономики, производительности и интеграции с существующими возможностями языков.
- JavaScript (ECMAScript 2024): Получил широкое распространение `Top-level await` (стандартизирован в ES2022), упрощающий инициализацию модулей[21]. Был добавлен новый статический метод `Array.fromAsync()` для создания массивов из асинхронных итерируемых объектов[22].
- Rust (Edition 2024): Ключевым достижением стала стабилизация асинхронных функций в трейтах (`async fn in traits`)[22], что решило одну из главных проблем асинхронной экосистемы языка. В версии 1.85.0 (февраль 2025 года) была также добавлена поддержка асинхронных замыканий (`async closures`)[23].
- C# (.NET 9): Был представлен метод `Task.WhenEach`, возвращающий `IAsyncEnumerable<Task<T>>` для обработки задач по мере их завершения. В C# 13 было снято ограничение на использование `ref`-переменных и `unsafe`-кода внутри `async`-методов (но не через границу `await`)[24]. Эксперименты с «зелёными потоками» были приостановлены в пользу улучшения существующей модели `async/await`[25].
- Python (Python 3.13): Главным нововведением стала экспериментальная сборка CPython без глобальной блокировки интерпретатора (GIL), что открыло возможность для истинного многопоточного параллелизма в задачах, интенсивно использующих процессор[26].
- JavaScript (ECMAScript 2025): Добавлен новый статический метод `Promise.try()` для безопасной обёртки синхронных или асинхронных операций в `Promise`[27]. Также были улучшены производительность и обработка ошибок в циклах `for await...of`[28].
- Zig: Представлена новая модель асинхронного ввода-вывода, которая вместо традиционного синтаксиса `async/await` использует разделение вызова функции и возврата из неё, позволяя выполнять несколько операций одновременно.
- Swift: Продолжилось осмысление совместного использования `async/await` и фреймворка `Combine`. `async/await` рекомендуется для последовательных задач (например, сетевые запросы), а `Combine` — для работы с потоками данных в реальном времени (события UI, данные с сенсоров).
- Node.js: Современные паттерны стали включать использование `async/await` в сочетании с новыми API, такими как `AsyncLocalStorage` для отслеживания состояния в асинхронных операциях[29].
Пример (C#)
Ниже представлен пример функции на C#, использующей async/await для асинхронной загрузки ресурса по URI и возвращающей его размер:
public async Task<int> FindSizeOfPageAsync(Uri uri)
{
HttpClient client = new();
byte[] data = await client.GetByteArrayAsync(uri);
return data.Length;
}
- Ключевое слово async показывает компилятору C#, что метод является асинхронным — то есть, внутри могут быть произвольные выражения
await, а результат работы метода возвращается как промис (класс Task<T>)[1]. - Тип возвращаемого значения Task<T> — в C# аналог промиса с результатом типа int.
- Первое выражение при вызове метода — new HttpClient().GetByteArrayAsync(uri)[30] которое также асинхронно: возвращает Task<byte[]>, и сразу запускает загрузку, возвращая неготовый результат.
- С помощью ключевого слова await выполнение функции будет приостановлено до разрешения Task, после чего дальнейший код продолжится с получением значения data.
- После завершения асинхронной операции функция возвращает data.Length, компилятор «оборачивает» возвращаемое значение в Task, вызывая колбэк у того, кто ожидал результат.[1],
Функция с async/await может содержать любое количество выражений await, каждое из которых будет обрабатываться аналогично. Функция может хранить промис вручную и выполнять дополнительную обработку до ожидания результата, а также агрегировать несколько промисов с помощью, например, Task.WhenAll()[1][30]. В большинстве реализаций промисов также доступны дополнительные возможности: отслеживание прогресса, многочисленные колбэки и т.д.
В языке C# (и во многих других со схожей конструкцией) паттерн async/await реализован не на уровне рантайма, а компилятором посредством трансформации в лямбды или продолжения. Например, рассмотрим вариант, на который может быть преобразован рассмотренный выше код:
public Task<int> FindSizeOfPageAsync(Uri uri)
{
HttpClient client = new();
Task<byte[]> dataTask = client.GetByteArrayAsync(uri);
Task<int> afterDataTask = dataTask.ContinueWith((originalTask) => {
return originalTask.Result.Length;
});
return afterDataTask;
}
Если методу интерфейса требуется возвращать промис, но внутри не нужны await, модификатор async не обязателен — промис возвращается напрямую (например Task.FromResult())[30].
Важная особенность: хотя код выглядит как блокирующий, в реальности выполнение не блокируется и может быть прервано внешними событиями, например, изменение разделяемого состояния между вызовами await:
string name = state.name;
HttpClient client = new();
byte[] data = await client.GetByteArrayAsync(uri);
// Возможна ошибка: state.a мог измениться внешним событием
Debug.Assert(name == state.name);
return data.Length;
Реализации
Язык C напрямую конструкций `await`/`async` не имеет. Вместо этого асинхронное программирование и функциональность корутин (сопрограмм) реализуются с помощью сторонних библиотек. По состоянию на 2024—2025 годы их можно разделить на несколько категорий.
К современным и активно развивающимся библиотекам, предоставляющим каждой корутине собственный стек (stackful coroutines), относятся libaco и libcoro. libaco позиционируется как высокопроизводительное решение с поддержкой разделяемых стеков для экономии памяти, а libcoro — как простая и портируемая библиотека.
Для систем с жёсткими ограничениями по памяти, например, для встраиваемых систем, используется легковесный подход без выделения отдельных стеков (stackless). Представителем этого подхода является библиотека Protothreads, реализованная на макросах. Её недостатком является то, что локальные переменные не сохраняются между приостановками, и состояние приходится хранить в статических или глобальных переменных.
Некоторые популярные в прошлом библиотеки, такие как libdill и libmill, считаются устаревшими и не рекомендуются для новых проектов.
Примером библиотеки, имитирующей ключевые слова `await`/`async` с помощью макросов, является s_task[31]:
#include <stdio.h>
#include "s_task.h"
constexpr int STACK_SIZE = 64 * 1024 / sizeof(int);
// Определяем память для стека задач
int g_stack_main[STACK_SIZE];
int g_stack0[STACK_SIZE];
int g_stack1[STACK_SIZE];
void sub_task(__async__, void* arg) {
int n = (int)(size_t)arg;
for (int i = 0; i < 5; ++i) {
printf("task %d, delay seconds = %d, i = %d\n", n, n, i);
s_task_msleep(__await__, n * 1000);
// s_task_yield(__await__);
}
}
void main_task(__async__, void* arg) {
// Запуск двух задач
s_task_create(g_stack0, sizeof(g_stack0), sub_task, (void*)1);
s_task_create(g_stack1, sizeof(g_stack1), sub_task, (void*)2);
for (int i = 0; i < 4; ++i) {
printf("task_main arg = %p, i = %d\n", arg, i);
s_task_yield(__await__);
}
// Ждём завершения обеих задач
s_task_join(__await__, g_stack0);
s_task_join(__await__, g_stack1);
}
int main(int argc, char* argv[]) {
s_task_init_system();
// Запуск главной задачи
s_task_create(g_stack_main, sizeof(g_stack_main), main_task, (void*)(size_t)argc);
s_task_join(__await__, g_stack_main);
printf("all task is over\n");
return 0;
}
В C++ оператор await (называемый co_await для ясности контекста корутин) официально включён в стандарт C++20[32]. Его поддерживают компиляторы GCC и MSVC; в Clang — частично.
Базовые классы std::promise и std::future сами по себе не выполняют требований для await; необходимо реализовать методы await_ready, await_suspend и await_resume в возвращаемом типе. Детали можно изучить на cppreference[33].
Пример класса AwaitableTask<T> с поддержкой co_await:
import std;
import org.wikipedia.util.AwaitableTask;
using org::wikipedia::util::AwaitableTask;
AwaitableTask<int> add(int a, int b) {
int c = a + b;
co_return c;
}
AwaitableTask<int> test() {
int ret = co_await add(1, 2);
std::println("Return {}", ret);
co_return ret;
}
int main() {
AwaitableTask<int> task = test();
return 0;
}
В стандарте C++23 не появилось кардинально новых механизмов асинхронности, однако были добавлены улучшения в стандартную библиотеку. Ключевым дополнением стал std::generator — первый стандартный тип корутины, который представляет собой простой способ создания генератора — функции, способной приостанавливать своё выполнение, возвращая последовательность значений[34]. Также был добавлен тип std::expected, предоставляющий удобный способ для возврата из функции либо успешного результата, либо ошибки, что является частым сценарием в асинхронных операциях[35].
Наиболее значительные изменения ожидаются в стандарте C++26, в который планируется включить модель Senders/Receivers (также известную как std::execution из предложения P2300)[36]. Изначально её принятие рассматривалось для C++23, но было отложено[37]. Эта модель предлагает высокоуровневую композитную абстракцию для управления асинхронными операциями. Основные концепции:
- Sender (отправитель) — легковесный объект, описывающий асинхронную операцию, но не запускающий её немедленно[38].
- Receiver (получатель) — объект с колбэками, который подписывается на результат Sender'а.
- Scheduler (планировщик) — абстракция, определяющая, где и как будет выполняться задача (например, в пуле потоков)[36].
Ожидается, что в C++26 оператор co_await можно будет напрямую применять к Sender-ам, что обеспечит бесшовную интеграцию корутин с новой моделью асинхронности[36].
В 2012 году в языке C# появился паттерн async/await (версия 5.0), — этот подход Microsoft называет task-based asynchronous pattern (TAP)[39]. Обычно асинхронные методы возвращают void, Task, Task<T>[30], ValueTask или ValueTask<T>[40].
В редких случаях могут использоваться пользовательские типы возврата (через кастомные async method builders)[41]. Методы, возвращающие void, предназначены для обработчиков событий; в других случаях лучше использовать Task для удобства обработки исключений[42].
- C# 6.0 (2015): Появилась возможность использовать оператор `await` в блоках `catch` и `finally`[43], что значительно упростило асинхронную обработку ошибок и очистку ресурсов.
- C# 7.0 / 7.1 (2017): Были введены обобщённые асинхронные возвращаемые типы. Ключевым примером стал тип-значение `ValueTask<T>`, позволяющий избежать выделения памяти в куче для методов, которые часто завершаются синхронно (например, при чтении из кэша)[44]. В C# 7.1 была добавлена поддержка асинхронного метода `Main`, что упростило запуск консольных приложений с асинхронными операциями на верхнем уровне[45].
- C# 8.0 (2019): Появились асинхронные потоки. Эта возможность включает интерфейс `IAsyncEnumerable<T>` для представления асинхронных последовательностей и конструкцию `await foreach` для их перебора[46][47]. Также был добавлен интерфейс `IAsyncDisposable` и оператор `await using` для асинхронного освобождения ресурсов[46].
- C# 10 (2021): Появилась возможность применять атрибут `[AsyncMethodBuilder]` непосредственно к методу, а не только к возвращаемому типу. Это предоставило разработчикам библиотек расширенные средства для оптимизации производительности, например, через создание пулов объектов-строителей асинхронных методов[48][49].
- C# 13 (2024): Было снято ограничение на использование типов `ref struct` (таких как `Span<T>`) и `unsafe`-кода внутри `async`-методов. Это позволяет писать более производительный код с меньшим количеством выделений памяти, однако такие переменные не могут использоваться через границу оператора `await`[50][51].
В F# с версии 2.0 (2007) поддержаны асинхронные рабочие процессы (asynchronous workflows), реализованные через вычислительные выражения (computation expressions)[52]. Ключевой особенностью этой модели, оказавшей влияние на C#, является концепция «холодных» задач. В отличие от «горячих» задач в C#, которые запускаются сразу, блок `async { ... }` в F# создаёт объект `Async<'T>`, который является лишь описанием вычисления и не запускается немедленно[53]. Для фактического выполнения необходимо явно вызвать одну из стартовых функций, например `Async.RunSynchronously` или `Async.StartAsTask`[54]. Внутри асинхронного блока для ожидания результата другой операции используются ключевые слова `let!` (для связывания результата с переменной) и `do!` (для выполнения операции, не возвращающей значимого результата)[55].
- F# 5 (2020): Была введена поддержка аппликативных вычислительных выражений с помощью ключевого слова `and!`. В отличие от последовательного `let!`, `and!` позволяет выполнять несколько независимых асинхронных операций параллельно, если это поддерживается строителем вычислительного выражения[56].
- F# 6 (2021): Появились более производительные вычислительные выражения `task { ... }`, которые напрямую работают с задачами .NET (`System.Threading.Tasks.Task<'T>`)[57]. Этот подход обеспечивает лучшую совместимость с C# и другими .NET-библиотеками, а также имеет меньшие накладные расходы и улучшенную поддержку отладки по сравнению с традиционными `async { ... }`[57]. Выражения `task { ... }` основаны на низкоуровневой функции «возобновляемый код» (resumable code) и рекомендуются для нового кода, взаимодействующего с экосистемой .NET[58].
Пример функции, скачивающей несколько страниц асинхронно с использованием традиционного `async`-выражения:
let asyncSumPageSizes (uris: #seq<Uri>) : Async<int> = async {
use httpClient = new HttpClient()
let! pages =
uris
|> Seq.map(httpClient.GetStringAsync >> Async.AwaitTask)
|> Async.Parallel
return pages |> Seq.fold (fun accumulator current -> current.Length + accumulator) 0
}
В языке Haskell отсутствует встроенная синтаксическая конструкция async/await, подобная той, что используется в C# или JavaScript. Вместо этого асинхронность реализуется на библиотечном уровне, где ключевую роль играет пакет async, созданный одним из ведущих разработчиков Haskell Саймоном Марлоу в 2012 году[59][60]. Эта библиотека предоставила более высокоуровневый и безопасный интерфейс по сравнению с базовым примитивом forkIO, использование которого было сопряжено с трудностями в обработке исключений и получении результата выполнения фоновой задачи[61].
Подход, предложенный библиотекой, концептуально схож с паттерном async/await и основан на нескольких ключевых функциях[62]:
async :: IO a -> IO (Async a)— запускает IO-действие в отдельном легковесном потоке и немедленно возвращает «обещание» результата типаAsync a[63]. Это аналог запуска асинхронной задачи.wait :: Async a -> IO a— ожидает завершения асинхронной операции и возвращает её результат. Если в ходе вычисления произошло исключение,waitповторно выбрасывает его в вызывающем потоке, что делает обработку ошибок предсказуемой[63]. Это является прямым аналогом оператораawait.
Сочетание этих функций внутри монадического блока do позволяет писать асинхронный код, который выглядит как последовательный.
Библиотека также реализует принципы структурного параллелизма. Функция withAsync гарантирует, что порождённый асинхронный поток будет автоматически отменён при выходе из блока, даже в случае исключения, что предотвращает утечки потоков[64]. Для композиции задач предусмотрены комбинаторы, такие как race (возвращает результат первой завершившейся задачи, отменяя остальные) и concurrently (выполняет две задачи параллельно и возвращает оба результата)[65].
Данный подход был подробно описан и популяризирован в книге Саймона Марлоу «Parallel and Concurrent Programming in Haskell» (2013)[66].
Пример параллельного выполнения двух операций:
import Control.Concurrent.Async (async, wait)
import Control.Concurrent (threadDelay)
import Text.Printf (printf)
-- Асинхронная операция, имитирующая долгую работу
longOperation :: String -> Int -> IO String
longOperation name delaySec = do
threadDelay (delaySec * 1000000) -- задержка в микросекундах
let result = printf "Операция '%s' завершена" name
return result
main :: IO ()
main = do
putStrLn "Запуск двух асинхронных операций..."
-- Запускаем обе операции параллельно, не дожидаясь их завершения
task1 <- async (longOperation "A" 2)
task2 <- async (longOperation "B" 1)
putStrLn "Операции запущены. Основной поток может выполнять другую работу."
-- Ожидаем результат сначала от второй, затем от первой операции
result2 <- wait task2
putStrLn result2
result1 <- wait task1
putStrLn result1
В языке Java отсутствует встроенный синтаксис async/await. Разработчики языка сознательно выбрали путь развития, альтернативный добавлению синтаксического сахара, сосредоточившись на фундаментальных улучшениях виртуальной машины Java (JVM)[67].
Долгое время основным инструментом для организации асинхронных операций служил класс `CompletableFuture`, представленный в Java 8. Он позволяет создавать цепочки асинхронных вычислений и считается ближайшим функциональным аналогом `async/await`[68][69].
Ключевым нововведением, ставшим общедоступным с выходом Java 21, стали виртуальные потоки (Virtual Threads), разработанные в рамках проекта Project Loom[70]. Этот подход позволяет писать код в привычном последовательном (синхронном) стиле, который при этом выполняется асинхронно и не блокирует потоки операционной системы. Виртуальные потоки являются легковесными, управляются самой JVM и предназначены в первую очередь для задач, связанных с вводом-выводом (I/O-bound), таких как сетевые запросы[71].
В дополнение к виртуальным потокам развивается концепция структурированной конкурентности (Structured Concurrency), которая в 2024 году находилась в статусе предварительной версии (Preview Feature). Она предлагает API `StructuredTaskScope` для управления группой связанных задач как единым целым, что упрощает обработку ошибок и отмену операций[67].
Несмотря на выбранный платформой путь, существуют сторонние библиотеки, которые реализуют синтаксис, подобный `async/await`, с помощью инструментов обработки байт-кода, однако они не являются частью стандартной платформы Java[72].
В JavaScript оператор await работает только внутри функции, отмеченной async, либо на верхнем уровне модуля. Если параметр — промис, выполнение возобновляется при его завершении (или возбуждается ошибка при отклонении), если нет — возвращается само значение[73]. Многие библиотеки возвращают совместимые промисы (Promises/A+). Однако, промисы из библиотеки jQuery до версии 3.0 не соответствовали стандарту[74].
Пример:
async function createNewDoc() {
let response = await db.post({});
return db.get(response.id);
}
async function main() {
try {
let doc = await createNewDoc();
console.log(doc);
} catch (err) {
console.log(err);
}
}
main();
С момента введения `async/await` в стандарте ECMAScript 2017, функциональность асинхронного программирования постоянно расширялась.
ECMAScript 2018 представил асинхронные итераторы и цикл for-await-of[75]. Это позволило работать с асинхронно поступающими данными (например, из потоков) в виде последовательных итераций. Также был добавлен метод Promise.prototype.finally(), который выполняет код после завершения промиса, независимо от его исхода (успех или ошибка)[76].
ECMAScript 2020 добавил метод Promise.allSettled(), который ожидает завершения всех промисов в массиве, возвращая массив объектов с их статусами и результатами, даже если некоторые из них были отклонены[77]. В отличие от `Promise.all()`, он не прерывается при первой ошибке. Также был стандартизирован динамический импорт `import()`, который возвращает промис и позволяет асинхронно загружать модули по требованию[78].
ECMAScript 2022 стандартизировал top-level await (верхнеуровневый `await`), который позволяет использовать `await` на верхнем уровне ES-модулей без необходимости оборачивать код в `async`-функцию[79]. Эта возможность, поддержка которой в основных браузерах и Node.js появилась в 2021 году, значительно упрощает асинхронную инициализацию модулей[79].
ECMAScript 2024 ввёл новый статический метод Array.fromAsync() для создания массивов из асинхронных итерируемых объектов.
В Node.js с версии 8 присутствуют удобные средства обёртывания callback-функций в промисы с помощью `util.promisify`[80]. Ключевые улучшения для асинхронного программирования были внесены в Node.js 12 (2019 год). Благодаря оптимизациям в движке V8, выполнение `async/await` стало значительно быстрее, в некоторых случаях превосходя по скорости код, написанный вручную с использованием промисов[81]. Кроме того, появились асинхронные стектрейсы, которые значительно упростили отладку: если раньше трассировка стека обрывалась на операторе `await`, то теперь она включает полную цепочку асинхронных вызовов, показывая весь контекст возникновения ошибки[82].
В языке Perl 5 синтаксис async/await не является встроенной возможностью ядра, а реализуется через модули из репозитория CPAN[83]. Ключевым является модуль Future::AsyncAwait, разработанный Полом Эвансом при поддержке гранта от The Perl Foundation в 2018 году[84].
Модуль предоставляет синтаксический сахар в виде ключевых слов async sub и await для работы с объектами Future, которые представляют отложенные вычисления[85]. Это позволяет писать асинхронный код в более простом и читаемом последовательном стиле, избегая сложных цепочек вызовов `->then()` (так называемого «ада колбэков»)[86]. Активная разработка модуля велась в 2019 году, в течение которого было выпущено множество версий с исправлениями и улучшениями[87]. К 2021—2025 годам модуль стал стабильным и активно поддерживаемым решением[88].
Future::AsyncAwait является синтаксической надстройкой и используется в связке с фреймворками для асинхронного ввода-вывода, такими как IO::Async[89]. Популярный веб-фреймворк Mojolicious также поддерживает `async/await` на его основе[90]. Несмотря на популярность этого подхода, синтаксис `async/await` не был включён в ядро языка, в том числе в стабильную версию Perl 5.42 (сентябрь 2025 года)[91].
В Python поддержка async/await реализована начиная с версии 3.5 (2015)[92], согласно PEP 492 (автор — Юрий Селиванов)[93].
С момента введения синтаксис и библиотека asyncio получили значительное развитие:
- Python 3.6 (2016): Были добавлены асинхронные генераторы (async def с yield, PEP 525) и асинхронные comprehensions (async for в списковых включениях, PEP 530)[94]. Модуль asyncio был признан стабильным, а не временным API[95].
- Python 3.11 (2022): Представлены группы задач (asyncio.TaskGroup), реализующие концепцию структурного параллелизма. Этот механизм, реализованный как асинхронный менеджер контекста (async with), гарантирует, что все порождённые задачи будут завершены до выхода из блока, и автоматически отменяет оставшиеся задачи при возникновении ошибки в одной из них[96]. Для обработки ошибок из нескольких задач одновременно были введены группы исключений (ExceptionGroup) и новый синтаксис except*[97].
- Python 3.12 (2023): Основной упор был сделан на повышение производительности (в некоторых тестах до 75 %)[98] и надёжности. Была улучшена предсказуемость отмены задач и добавлена экспериментальная возможность «нетерпеливого» выполнения задач (Eager Task Execution) через asyncio.eager_task_factory(), которая позволяет задачам начать выполняться немедленно в момент создания, если цикл событий ожидает[99].
- Python 3.13 (2024): Главным нововведением стала экспериментальная сборка CPython без глобальной блокировки интерпретатора (GIL), предложенная в PEP 703. Это открыло возможность для истинного многопоточного параллелизма в задачах, интенсивно использующих процессор, и дало разработчикам новый инструмент для конкурентного выполнения кода наряду с asyncio[100].
- Python 3.14 (2025): Были добавлены встроенные инструменты для интроспекции и отладки. Команды python -m asyncio ps и python -m asyncio pstree позволяют просматривать активные асинхронные задачи в работающем процессе[101]. Для интерактивной отладки появилась функция pdb.set_trace_async(), позволяющая использовать await внутри сессии отладчика pdb[102]. Кроме того, в сборках без GIL цикл событий asyncio стал потокобезопасным[102].
import asyncio
async def main() -> None:
print("hello")
await asyncio.sleep(1)
print("world")
if __name__ == "__main__":
asyncio.run(main())
7 ноября 2019 года `async/await` стали доступны в стабильной версии Rust 1.39.0[103]. Асинхронные функции преобразуются компилятором в функции, возвращающие объект, реализующий трейт `Future` (и реализуются как конечный автомат)[104].
Ключевым этапом для экосистемы стал выпуск в декабре 2020 года асинхронной среды выполнения (runtime) Tokio версии 1.0, что ознаменовало готовность `async/await` для широкого промышленного использования[105]. Tokio предоставляет инструменты для написания сетевых приложений и сервисов, а макрос `#[tokio::main]` упрощает запуск асинхронных программ. Долгое время альтернативой выступала среда `async-std`, однако со временем экосистема в значительной степени консолидировалась вокруг Tokio[105].
- Rust 1.75 (декабрь 2023): Была стабилизирована одна из самых ожидаемых возможностей — использование `async fn` в трейтах (`async fn in traits`). Это решило одну из главных проблем асинхронного Rust, позволив создавать более гибкие и универсальные абстракции для асинхронного кода.
- Rust 1.85 (февраль 2025): В версии, стабилизировавшей редакцию языка Rust 2024, была добавлена поддержка асинхронных замыканий (`async closures`). Это позволяет создавать замыкания вида `async {}`, которые возвращают `Future`, что упрощает использование асинхронного кода в функциональных паттернах.
// В Cargo.toml: tokio = { version = "1", features = ["full"] }
use std::time::Duration;
async fn first_task() {
println!("Задача 1 началась");
tokio::time::sleep(Duration::from_secs(2)).await;
println!("Задача 1 завершилась");
}
async fn second_task() {
println!("Задача 2 началась");
tokio::time::sleep(Duration::from_secs(1)).await;
println!("Задача 2 завершилась");
}
#[tokio::main]
async fn main() {
// Запускаем обе задачи параллельно
tokio::join!(first_task(), second_task());
}
C версии 5.5 (2021)[106] в Swift поддерживается async/await согласно спецификации SE-0296[107].
- Swift 5.7 (2022): Был выпущен open-source пакет Swift Async Algorithms, расширяющий возможности работы с асинхронными последовательностями (AsyncSequence) с помощью таких операторов, как `zip`, `merge`, `throttle` и `debounce`[108]. Также были представлены новые API для работы со временем (`Clock`, `Instant`, `Duration`), что позволило улучшить управление асинхронными задачами, например, через обновлённую функцию `Task.sleep`[108].
- Swift 5.9 (2023): Были представлены группы отбрасываемых задач (Discarding Task Groups), которые решают проблему накопления результатов и потенциальной утечки памяти в долгоживущих серверных приложениях[109]. Также появилась возможность создавать пользовательские исполнители акторов (custom actor executors) для низкоуровневого управления контекстом выполнения акторов[109].
- Swift 6 (2024): Ключевым изменением стало включение строгой проверки конкурентности по умолчанию. Предупреждения о потенциальных гонках данных (data races) стали ошибками компиляции, что заставляет разработчиков обеспечивать безопасность доступа к данным на этапе написания кода[110][111].
- Swift 6.2 (2025): В рамках концепции «доступной многопоточности» (Approachable Concurrency) было изменено поведение по умолчанию: теперь асинхронная функция продолжает выполняться в том же контексте (акторе), из которого была вызвана, что делает код более предсказуемым, особенно при работе с `@MainActor`[112]. Для явного указания, что функция должна выполняться в фоновом потоке, был добавлен атрибут `@concurrent`[113].
func getNumber() async throws -> Int {
try await Task.sleep(nanoseconds: 1_000_000_000)
return 42
}
Task {
let first = try await getNumber()
let second = try await getNumber()
print(first + second)
}
В TypeScript поддержка `async/await` была реализована в версии 1.7 (2015). Ключевым развитием стал выход TypeScript 2.1 (декабрь 2016), в котором появилась возможность компиляции (`downleveling`) конструкций `async/await` для более старых версий JavaScript (ES3 и ES5). Это сделало данную возможность доступной для использования в большинстве браузеров при условии наличия полифилла для `Promise`[114]. Для этого компилятор преобразует асинхронную функцию в машину состояний с использованием генераторов и вспомогательной функции `__awaiter`[114].
Дальнейшее развитие было направлено на улучшение работы с типами и интеграцию с новыми возможностями ECMAScript. В TypeScript 4.5 (ноябрь 2021) были представлены[115]:
- `Awaited<T>` — новый служебный тип, который рекурсивно «разворачивает» промисы для получения конечного типа значения. Он используется для улучшения вывода типов в таких методах, как `Promise.all`[116][115].
- `Top-level await` — поддержка `await` на верхнем уровне ES-модулей без необходимости оборачивать код в `async`-функцию, что упрощает асинхронную инициализацию. Для этого требуется опция компилятора `--module es2022`[117][115].
В TypeScript 5.2 (август 2023) была добавлена поддержка явного управления асинхронными ресурсами с помощью конструкции `await using`[118]. Этот синтаксис автоматически вызывает и ожидает завершения метода, определённого в `Symbol.asyncDispose`, при выходе из блока, что обеспечивает надёжное асинхронное освобождение ресурсов[119].
class DatabaseConnection implements AsyncDisposable {
async connect() { /* ... */ }
async close() { /* ... */ }
async [Symbol.asyncDispose]() {
await this.close();
}
}
async function queryDatabase() {
await using db = new DatabaseConnection();
await db.connect();
// ... работа с базой данных
} // db.close() будет вызван и дождан здесь автоматически
Преимущества и критика
Поддержка async/await особенно интересна для языков, не имеющих управляемого рантайма: реализации полностью ограничиваются трансформацией функций в конечный автомат на стадии компиляции[120].
Сторонники async/await утверждают, что синтаксис позволяет писать неблокирующий асинхронный код, очень похожий на традиционный синхронный, что повышает удобство и читаемость и делает код с await почти столь же простым для понимания, как блокирующий[121]. Это облегчает создание надёжного асинхронного ПО, требующего сложной обработки событий.
Критики async/await отмечают, что паттерн «распространяется» на окружающий код, порождая проблему разделения экосистемы библиотек на синхронные и асинхронные («окрашивание функций», function coloring)[122]. Альтернативой являются «бесцветные» (colorless) модели (например, гороутины Go или виртуальные потоки Java)[123]. Показательным примером поиска решения этой проблемы являются эксперименты команды .NET с «зелёными потоками», которые могли бы устранить необходимость в async/await. Однако в 2024 году было решено приостановить эту работу в пользу улучшения существующей модели из-за сложности внедрения второго подхода к параллелизму и проблем с производительностью.


