Как вызвать асинхронный метод внутри синхронного метода в C#?

Ответ

Вызов асинхронного метода из синхронного контекста — это антипаттерн, которого следует избегать. Лучшее решение — сделать всю цепочку вызовов асинхронной. Если это невозможно, используйте следующие подходы с пониманием их рисков.

1. Task.Run для CPU-bound операций Используйте, если асинхронный метод выполняет интенсивные вычисления, а не I/O.

public void SyncMethod()
{
    // Запускаем задачу в пуле потоков и синхронно ожидаем результат
    Task.Run(() => AsyncCpuBoundOperation()).GetAwaiter().GetResult();
}

private async Task AsyncCpuBoundOperation()
{
    await Task.Run(() => HeavyComputation());
}

2. .GetAwaiter().GetResult() для I/O операций (с осторожностью) Может привести к взаимоблокировкам (deadlock), особенно в контекстах с SynchronizationContext (например, UI-поток в WPF/WinForms или HttpContext в старых версиях ASP.NET).

public void SyncMethod()
{
    // Риск дедлока! Не используйте в UI-потоке или ASP.NET (не Core).
    AsyncIoMethod().GetAwaiter().GetResult();
}

3. Запуск без ожидания (Fire-and-forget) Подходит только для фоновых операций, результат которых не нужен вызывающему коду. Опасен потерей исключений.

public void SyncMethod()
{
    // Просто запускаем задачу. Исключения могут быть не обработаны.
    _ = AsyncMethod();
}

Ключевые рекомендации:

  • Избегайте Task.Wait() и Task.Result — они оборачивают исключения в AggregateException и также подвержены дедлокам.
  • В ASP.NET Core используйте асинхронные контроллеры и методы (async Task<IActionResult>). Синхронные обертки нарушают масштабируемость.
  • В библиотечном коде предоставляйте как асинхронные (DoWorkAsync), так и синхронные (DoWork) версии методов, если это необходимо, где синхронная версия является оберткой.
  • Рассмотрите IAsyncDisposable и IAsyncEnumerable<T> для полной асинхронной интеграции.

Ответ 18+ 🔞

Ну вот смотри, опять эта вечная тема — как вызвать асинхронную хрень из синхронного говнокода. Люди, блядь, как будто специально ищут, где бы наступить на грабли. Ладно, разжую, но ты запомни главное: лучше вообще так не делать, а переделать всё на async/await. Но если уж совсем пиздец и нельзя, то вот варианты, с рисками и подводными камнями.

1. Task.Run для тяжёлых вычислений (CPU-bound) Это если твой асинхронный метод не ждёт диск или сеть, а реально грузит процессор. Суть — выкидываем работу в пул потоков и тупо ждём.

public void SyncMethod()
{
    // Запулили в фон, притормозили и ждём, пока не выплюнет результат
    Task.Run(() => AsyncCpuBoundOperation()).GetAwaiter().GetResult();
}

private async Task AsyncCpuBoundOperation()
{
    await Task.Run(() => HeavyComputation()); // Сама работа тоже в пуле
}

Но если такую штуку влепить в UI-поток, то интерфейс повиснет, будто ему на шею гирю повесили. Исключения, кстати, вылетят нормальные, не завернутые в AggregateException.

2. .GetAwaiter().GetResult() для I/O операций (осторожно, ебёный рот!) Вот это — самый частый источник дедлоков, особенно если вокруг есть какой-нибудь SynchronizationContext (как в старом ASP.NET или в WPF/WinForms). Представь: поток ждёт, когда задача завершится, а задача хочет вернуться в этот же поток, но он занят — ожиданием. Пиздец, замок. Всё встало.

public void SyncMethod()
{
    // Риск дедлока — как сыграть в русскую рулетку с полным барабаном
    AsyncIoMethod().GetAwaiter().GetResult();
}

В ASP.NET Core, кстати, чуть безопаснее, потому что там по умолчанию нет этого контекста, но расслабляться не стоит.

3. Запустил и забыл (Fire-and-forget) Это когда тебе похер на результат, лишь бы отстал. Например, отправил логирование куда-то и пошёл дальше. Но, блядь, исключения в таком случае просто сгорят, и ты даже не узнаешь, что всё упало.

public void SyncMethod()
{
    // Пустил задачу в свободное плавание. Упала — твои проблемы.
    _ = AsyncMethod();
}

Если используешь такой подход, обязательно настрой продолжение с обработкой ошибок, иначе потом будешь чесать репу, почему ничего не работает.

Что запомнить, чтобы не выстрелить себе в ногу:

  • Task.Wait() и Task.Result — говно. Они пакуют исключения в AggregateException, и с ними тоже можно попасть в дедлок. Беги от них как чёрт от ладана.
  • В ASP.NET Core делай всё асинхронно, от контроллера до базы. Иначе сервер будет масштабироваться как черепаха в смоле.
  • Если пишешь библиотеку и хочешь угодить всем, делай две версии метода: асинхронную DoWorkAsync() и синхронную-обёртку DoWork(). Но внутри синхронной — аккуратно, с пониманием, где она будет работать.
  • Не забывай про IAsyncDisposable и IAsyncEnumerable<T>. Если уж пошла такая пьянка, делай всё полностью асинхронно, без полумер.

Короче, идеальный вариант — не бороться с системой, а плыть по течению. Переделывай всё на async, и спи спокойно.