Синхронный вызов асинхронного метода?
Возможно, это дубликат, но я не смог найти однозначного ответа. Да и много рецептов попадается для старых версий .NET Framework
, а в современном .NET (Core)
что-то могло поменяться.
Итак, имеется легаси код, в котором понатыканы конструкции вида:
FooAsync().Wait();
...
var result = BarAsync().Result;
Мне кажется, что такое обращение с асинхронными методами не является правильным. Где-то мне удалось протащить асинхронность через все уровни и исправить это безобразие на нормальные вызовы через await
. Но так удалось сделать далеко не везде, где-то на верхнем уровне всё упирается в синхронный код и я не уверен, что я могу его там поменять на асинхронный. Либо там очень много уровней архитектуры где-то выше, которые я тоже не могу сразу все поменять.
Итак, вопрос: как в современных версиях .NET
рекомендуется вызывать асинхронный код из синхронного? Я понимаю, что так то это вообще не рекомендуется, но вот для случаев, когда такого не избежать - как это сделать наиболее правильно? Или может смириться с тем как есть, другие варианты будут ничем не лучше?
P.S. В частности проблема есть с вызовом асинхронного метода в конструкторе класса. Да, есть методики для этого с сокрытием конструктора, но мне кажется, что их невозможно совместить с использованием DI
фреймворка. Пример:
public class Foo : IFoo
{
public Foo(ISomething something, IAnother another)
{
_something = something;
_another = another;
Bar().Wait();
}
}
...
serviceCollection.AddSingleton<ISomething, Something>();
serviceCollection.AddSingleton<IAnother, Another>();
serviceCollection.AddSingleton<IFoo, Foo>();
Ответы (1 шт):
На самом деле, единственное место, где sync-over-async допустим и используется - это main
, точка входа в приложение. Но для того чтобы никого не смущать, разработчики .NET уже реализовали обвязку, и теперь можно писать просто.
static async Task Main(string[] args)
{
// await
}
Вот он, верхний уровень. Других верхних уровней нет.
Конечно, есть например Windows Forms, где базовые обработчики событий не поддерживают асинхронность, например
private void Button_Click(object sender, EventArgs e)
{
// Foo();
}
Но и здесь уже есть готовое решение. Так как асинхронный метод не блокирует UI на время выполнения, следует защитить кнопку до конца выполнения асинхронной операции. Плюс так как кроме void
вернуть ничего из метода нельзя, нужно перехватить исключения в async void
методе.
private async void Button_Click(object sender, EventArgs e)
{
Button btn = (Button)sender;
btn.Enabled = false;
try
{
// await FooAsync();
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
btn.Enabled = true;
}
Собственно вот и всё решение. Никаких .Wait()
или .Result
в коде быть по определению не должно.
Единственный случай, когда разрешается обращение к .Result
это когда Task
уже явно завершён, и даже в некоторых случаях это считается более здоровой практикой, чем await
, но это уже тонкие тонкости.
if (task.IsCompleted)
{
var result = task.Result;
}
Поэтому в случае, если вы отдаёте себе отчёт в том что делаете, то как временные костыли можете использовать .Wait()
или .Result
до тех пор, пока планомерно всё это не перепишете полностью.
Использовать sync-over-async только потому что кажется, что других простых решений нет, крайне не рекомендуется ни при каких обстоятельствах. Ибо в лучшем случае это будет слив ресурсов из-за бессмысленной блокировки потока, в худшем - будете искать дедлоки, возникающие только в полнолуние при параде планет.
P. S. В случае с конструкторами решений тоже несколько. Один из них "запустил и забыл".
private readonly _initTask;
private string _superImportantValue;
public Foo(ISomething something, IAnother another)
{
_something = something;
_another = another;
_initTask = Bar();
}
private async Task Bar()
{
await Task.Delay(1000);
_superImportantValue = "ready";
}
Теперь надо в методе, использующем _superImportantValue
обработать вероятность ошибки.
Вариант 1 - бросить исключение
public void Baz()
{
if (!_initTask.IsCompleted)
throw new InvalidOperationException();
// ... _superImportantValue
}
Вариант 2 - всё-таки замёрзнуть
public void Baz()
{
_initTask.Wait();
// ... _superImportantValue
}
Такой вариант допустим, если по логике предполагается, что в 99,9% случаев _initTask
на момент вызова метода уже готов.
В любом случае если это не идеальное решение, то оно хотя-бы не блочит вам построение приложения в IOC контейнере.
Вариант 3 - а что, так можно было?
public async Task BazAsync()
{
await _initTask;
// ... _superImportantValue
}