TGViewer
Библиотека шарписта | C#, F#, .NET, ASP.NET Библиотека шарписта | C#, F#, .NET, ASP.NET @csharpproglib · 21.7K subscribers
Post #7014 2.96K
Урок истории

До .NET 4.5 асинхронный код писали вручную через обратные вызовы и явный маршалинг между потоками. Это было сложно, многословно и доступно только опытным разработчикам. C# 5 и Visual Basic с новым синтаксисом async/await убрали эту сложность.

Что происходит внутри

Компилятор превращает каждый async-метод в конечный автомат. Вот простой пример:
public static async Task SimpleBodyAsync() {
Console.WriteLine("Hello, Async World!");
}


После компиляции это превращается примерно в:
public static Task SimpleBodyAsync() {
var d = new <SimpleBodyAsync>d__0();
d.<>t__builder = AsyncTaskMethodBuilder.Create();
d.MoveNext();
return d.<>t__builder.Task;
}


Плюс отдельная struct с методом MoveNext, блоком try/catch и полями для хранения состояния. JIT не сможет встроить такой метод по месту вызова. Появляются издержки на вызов методов инфраструктуры SetResult, SetException и запись в поля конечного автомата.

Когда async лишний

Если метод всегда выполняется синхронно, оборачивать его в async нет смысла. Тауб приводит пример MemoryStream.ReadAsync: чтение из памяти и без того быстрое, и каждый вызов будет создавать новый объект Task<int> просто чтобы вернуть число.

Решение: возвращать кешированный результат вручную.
private Task<int> m_lastTask;

public override Task<int> ReadAsync(
byte[] buffer, int offset, int count,
CancellationToken cancellationToken)
{
int numRead = this.Read(buffer, offset, count);
return m_lastTask != null && numRead == m_lastTask.Result
? m_lastTask
: (m_lastTask = Task.FromResult(numRead));
}


Это позволяет при повторяющихся вызовах с одним и тем же результатом не создавать новые объекты совсем.

SynchronizationContext и лишние переходы

По умолчанию await захватывает текущий SynchronizationContext и возвращает продолжение в него. Для UI-потока это удобно: не нужно вручную делать маршалинг. Но в библиотечном коде это создаёт лишние переходы между потоками.

Если из UI-потока запустить цикл копирования с await на каждой операции чтения и записи мегабайта данных, получится более 500 переходов из фоновых потоков обратно в UI-поток. Чтобы этого избежать, в библиотеках следует использовать ConfigureAwait(false):

while ((numRead = await source.ReadAsync(buffer, 0, buffer.Length)
.ConfigureAwait(false)) > 0)
{
await destination.WriteAsync(buffer, 0, numRead)
.ConfigureAwait(false);
}


Без ConfigureAwait(false) в библиотечном коде возможна и взаимоблокировка: если вызывающий код в UI-потоке зовёт t.Wait(), а продолжение пытается вернуться в тот же заблокированный поток, оба будут ждать друг друга бесконечно.

Локальные переменные и сбор мусора

Компилятор поднимает все локальные переменные асинхронного метода в поля конечного автомата, который упаковывается в кучу при первом реальном ожидании. На момент выхода статьи компиляторы поднимали иногда больше переменных, чем нужно: даже те, что после await уже не читались.

// Лишнее поле в конечном автомате
public static async Task FooAsync() {
var dto = DateTimeOffset.Now;
var dt = dto.DateTime;
await Task.Yield();
Console.WriteLine(dt);
}

// Лучше так:
public static async Task FooAsync() {
var dt = DateTimeOffset.Now.DateTime;
await Task.Yield();
Console.WriteLine(dt);
}


Чем больше объектов создаётся, тем чаще срабатывает сборщик мусора. Это влияет на всю систему, а не только на конкретный метод.

Меньше await — лучше

Каждое await-выражение несёт накладные расходы. Если нужно подождать несколько задач, лучше объединить их через Task.WhenAll, чем ждать по одной:

// Хуже: три отдельных await
int ra = await a;
int rb = await b;
int rc = await c;

// Лучше: одно await на все три
int[] results = await Task.WhenAll(a, b, c);


async/await упростил жизнь разработчикам, но не отменил необходимость понимать, что происходит внутри.

➡️ Блог разработчиков

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#sharp_view
  • ❤ 8
  • 🔥 5
  • 🤔 2
  • ⚡ 1
More from @csharpproglib
  1. Sep 23, 2026⚙️ yield return не бесплатный Итераторы выглядят просто, но работают иначе: IEnumerable<in…
  2. Sep 22, 2026💡 Replace, Regex или StringBuilder? Для замены текста в C# есть несколько инструментов. И…
  3. Sep 21, 2026⚙️ Настоящие атомарные операции Если Volatile решает проблему видимости, то Interlocked ре…
  4. Sep 20, 2026💪 Разминка перед трудовыми буднями Что произойдёт? ❤️ — список станет [1, 3] 🔥 — Invalid…
  5. Sep 19, 2026🧩 Middleware в ASP.NET Core: 3 ловушки Middleware — звено HTTP pipeline: app.Use(async (c…
  6. Sep 18, 2026📍 Навигация: Вакансии • Задачи • Собесы 🐸Библиотека шарписта #garbage_collector
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →