await
по умолчанию захватывает текущий контекст и использует его для возобновления метода async
.Этот контекст равен SynchronizationContext.Current
, если только он не равен null
, в этом случае он равен TaskScheduler.Current
.
. В этом случае await
захватывает LimitedConcurrencyLevelTaskScheduler
, используемый для выполнения DoWork
.Таким образом, после запуска Task.Delay
оба раза оба этих потока блокируются (из-за GetAwaiter().GetResult()
).Когда Task.Delay
завершается, await
планирует остаток метода HttpClientGetAsync
в своем контексте.Однако контекст не запустит его, так как у него уже есть 2 потока.
Таким образом, вы заканчиваете тем, что потоки заблокированы в контексте, пока не завершены их методы async
, но методы async
не могут завершиться, пока не будет выполненосвободная тема в контексте;таким образом тупик.Очень похоже на стандартный тупик "не блокировать по асинхронному коду" , только с потоками n вместо одного.
Пояснения:
Кажется, проблема в том, что Task.Delay (2000) никогда не завершается.
Task.Delay
завершается, но await
не может продолжить выполнение метода async
.
Если я установлю maxDegreeOfParallelism на 3 (или больше), он завершится.Но с maxDegreeOfParallelism = 2 (или меньше) я предполагаю, что нет потока, доступного для выполнения задачи.Почему это так?
Доступно множество тем.Но LimitedConcurrencyTaskScheduler
позволяет одновременно запускать только 2 потока в его контексте.
Похоже, это связано с async / await, поскольку, если я удаляю его и просто выполняю Task.Delay (2000).GetAwaiter (). GetResult () в DoWork работает отлично.
Да;это await
, который захватывает контекст.Task.Delay
не захватывает контекст внутри, поэтому его можно завершить, не вводя LimitedConcurrencyTaskScheduler
.
Решение:
Обычно планировщики задач не очень хорошо работают с асинхронным кодом.Это связано с тем, что планировщики задач были разработаны для параллельных задач, а не для асинхронных задач.Таким образом, они применяются только тогда, когда код работает (или заблокирован).В этом случае LimitedConcurrencyLevelTaskScheduler
только «считает» код, который работает;если у вас есть метод, который выполняет await
, он не будет «считаться» с этим пределом параллелизма.
Итак, ваш код оказался в ситуации, когда он имеет антипаттерн синхронизации-синхронизации-асинхронностиВозможно, из-за того, что кто-то пытался избежать проблемы с await
, работающей не так, как ожидалось, с ограниченным количеством планировщиков параллельных задачЭтот антипаттерн синхронизации через асинхронный вызов вызвал проблему взаимоблокировки.
Теперь вы можете добавить больше хаков, используя повсеместно ConfigureAwait(false)
, и продолжить блокировку асинхронного кода, или вы можете исправить это лучше.
Более правильным решением было бы выполнение асинхронного регулирования.Полностью выбросить LimitedConcurrencyLevelTaskScheduler
;планировщики задач с ограничением параллелизма работают только с синхронным кодом, а ваш код асинхронный.Вы можете выполнить асинхронное регулирование с помощью SemaphoreSlim
, например:
class TaskSchedulerTest
{
private readonly SemaphoreSlim _mutex = new SemaphoreSlim(2);
public async Task RunAsync()
{
var tasks = Enumerable.Range(1, 2).Select(id => DoWorkAsync(id));
await Task.WhenAll(tasks);
}
private async Task DoWorkAsync(int id)
{
await _mutex.WaitAsync();
try
{
Console.WriteLine($"Starting Work {id}");
await HttpClientGetAsync();
Console.WriteLine($"Finished Work {id}");
}
finally
{
_mutex.Release();
}
}
async Task HttpClientGetAsync()
{
await Task.Delay(2000);
}
}