Вызов методов асинхронизации из неасинхронного кода
Я в процессе обновления библиотеки, которая имеет поверхность API, которая была построена в .NET 3.5. В результате все методы синхронны. Я не могу изменить API (т.е. Преобразовать возвращаемые значения в Task), потому что это потребует изменения всех вызывающих абонентов. Поэтому я остался с тем, как наилучшим образом использовать методы async синхронно. Это в контексте приложений ASP.NET 4, ASP.NET Core и .NET/.NET Core.
Возможно, я не был достаточно ясен - ситуация в том, что у меня есть существующий код, который не является асинхронным, и я хочу использовать новые библиотеки, такие как System.Net.Http и AWS SDK, которые поддерживают только асинхронные методы. Поэтому мне нужно устранить пробел и иметь код, который можно вызвать синхронно, но затем может вызывать методы async в другом месте.
Я много читал, и есть несколько раз, на что это было задано и ответили.
Вызов метода асинхронизации из неасинхронного метода
Синхронное ожидание операции async и почему Wait() заморозит программу здесь
Вызов метода асинхронного синхронного метода
Как выполнить задачу async <T> метод синхронно?
Синхронно вызов метода асинхронного подключения
Как вызвать асинхронный метод из синхронного метода в С#?
Проблема в том, что большинство ответов разные! Наиболее распространенный подход, который я видел, это использовать. Результат, но это может быть взаимоблокировка. Я пробовал все следующее, и они работают, но я не уверен, что это лучший подход, чтобы избежать взаимоблокировок, иметь хорошую производительность и хорошо играть со временем выполнения (с точки зрения соблюдения планировщиков задач, вариантов создания задач и т.д.). Есть ли окончательный ответ? Каков наилучший подход?
private static T taskSyncRunner<T>(Func<Task<T>> task)
{
T result;
// approach 1
result = Task.Run(async () => await task()).ConfigureAwait(false).GetAwaiter().GetResult();
// approach 2
result = Task.Run(task).ConfigureAwait(false).GetAwaiter().GetResult();
// approach 3
result = task().ConfigureAwait(false).GetAwaiter().GetResult();
// approach 4
result = Task.Run(task).Result;
// approach 5
result = Task.Run(task).GetAwaiter().GetResult();
// approach 6
var t = task();
t.RunSynchronously();
result = t.Result;
// approach 7
var t1 = task();
Task.WaitAll(t1);
result = t1.Result;
// approach 8?
return result;
}
Ответы
Ответ 1
Итак, я остался с тем, как наилучшим образом использовать методы async синхронно.
Во-первых, это нормально. Я заявляю об этом, потому что в Qaru часто указывается на это как на дьявола как на одеяло, без учета конкретного случая.
Не нужно быть асинхронным для правильности. Блокирование чего-то асинхронного, чтобы сделать его синхронизацией, имеет стоимость исполнения, которая может иметь значение или может быть совершенно неактуальной. Это зависит от конкретного случая.
Тупики происходят из двух потоков, пытающихся одновременно войти в один однопоточный контекст синхронизации. Любой метод, который позволяет избежать этого, позволяет избежать блокировок, вызванных блокировкой.
Здесь все ваши вызовы .ConfigureAwait(false)
бессмысленны, потому что вы не ожидаете.
RunSynchronously
неверно использовать, потому что не все задачи могут быть обработаны таким образом.
.GetAwaiter().GetResult()
отличается от Result/Wait()
тем, что он имитирует поведение распространения исключений await
. Вам нужно решить, хотите ли вы этого или нет. (Итак, исследуйте, что это такое, нет необходимости повторять его здесь.)
Кроме того, все эти подходы имеют одинаковую производительность. Они будут выделять событие ОС так или иначе и блокировать его. Это дорогая часть. Я не знаю, какой подход абсолютно дешев.
Мне лично нравится шаблон Task.Run(() => DoSomethingAsync()).Wait();
, потому что он категорически избегает тупиков, прост и не скрывает некоторые исключения, которые может скрывать GetResult()
. Но вы можете использовать GetResult()
, а также с этим.
Ответ 2
Я в процессе обновления библиотеки, которая имеет поверхность API, которая была построена в .NET 3.5. В результате все методы синхронны. Я не могу изменить API (т.е. Преобразовать возвращаемые значения в Task), потому что это потребует изменения всех вызывающих абонентов. Поэтому я остался с тем, как наилучшим образом использовать методы async синхронно.
Нет универсального "лучшего" способа выполнения анти-шаблона sync-over-async. Только различные хаки, у каждого из которых есть свои недостатки.
Я рекомендую, чтобы вы сохраняли старые синхронные API, а затем добавляли асинхронные API вместе с ними. Вы можете сделать это, используя "boolean argument hack" , как описано в моей статье MSDN о Brownfield Async.
Во-первых, краткое объяснение проблем с каждым подходом в вашем примере:
-
ConfigureAwait
имеет смысл только тогда, когда существует await
; в противном случае он ничего не делает.
-
Result
завершает исключения в AggregateException
; если вы должны заблокировать, используйте GetAwaiter().GetResult()
вместо этого.
-
Task.Run
выполнит свой код в потоке пула потоков (очевидно). Это нормально , только если код может работать в потоке пула потоков.
-
RunSynchronously
- это расширенный API, используемый в чрезвычайно редких ситуациях при выполнении динамического parallelism на основе задач. Вы вообще не в этом сценарии.
-
Task.WaitAll
с одной задачей совпадает с просто Wait()
.
-
async () => await x
- это менее эффективный способ сказать () => x
.
- Блокирование задачи, запущенной из текущего потока может вызвать взаимоблокировки.
Здесь пробой:
// Problems (1), (3), (6)
result = Task.Run(async () => await task()).ConfigureAwait(false).GetAwaiter().GetResult();
// Problems (1), (3)
result = Task.Run(task).ConfigureAwait(false).GetAwaiter().GetResult();
// Problems (1), (7)
result = task().ConfigureAwait(false).GetAwaiter().GetResult();
// Problems (2), (3)
result = Task.Run(task).Result;
// Problems (3)
result = Task.Run(task).GetAwaiter().GetResult();
// Problems (2), (4)
var t = task();
t.RunSynchronously();
result = t.Result;
// Problems (2), (5)
var t1 = task();
Task.WaitAll(t1);
result = t1.Result;
Вместо любого из этих подходов, поскольку у вас есть действующий синхронный код, вы должны использовать его вместе с более новым естественно-асинхронным кодом. Например, если ваш существующий код использовал WebClient
:
public string Get()
{
using (var client = new WebClient())
return client.DownloadString(...);
}
и вы хотите добавить асинхронный API, тогда я бы сделал это следующим образом:
private async Task<string> GetCoreAsync(bool sync)
{
using (var client = new WebClient())
{
return sync ?
client.DownloadString(...) :
await client.DownloadStringTaskAsync(...);
}
}
public string Get() => GetCoreAsync(sync: true).GetAwaiter().GetResult();
public Task<string> GetAsync() => GetCoreAsync(sync: false);
или, если вы по какой-то причине должны использовать HttpClient
:
private string GetCoreSync()
{
using (var client = new WebClient())
return client.DownloadString(...);
}
private static HttpClient HttpClient { get; } = ...;
private async Task<string> GetCoreAsync(bool sync)
{
return sync ?
GetCoreSync() :
await HttpClient.GetString(...);
}
public string Get() => GetCoreAsync(sync: true).GetAwaiter().GetResult();
public Task<string> GetAsync() => GetCoreAsync(sync: false);
При таком подходе ваша логика войдет в методы Core
, которые могут выполняться синхронно или асинхронно (как определено параметром sync
). Если sync
- true
, то основные методы должны возвращать уже выполненную задачу. Для реализации используйте синхронные API для синхронного запуска и используйте асинхронные API для асинхронного запуска.
В конце концов, я рекомендую отказаться от синхронных API.