Livelock и подавленная асинхронность
Обнаружена интересная ситуация livelock, связанная с асинхронностью.
Рассмотрим приведенный ниже код, который вызывает livelock и выполняется в течение 1 минуты, хотя полезная полезная нагрузка практически не запускается. Причина, по которой время выполнения составляет около 1 минуты, состоит в том, что мы фактически достигнем предела увеличения пула потоков (около 1 потока в секунду), поэтому 300 итераций будут запускать его в течение 5 минут.
Это не тривиальный тупик, когда мы синхронно ожидаем асинхронную операцию в среде с SyncronizationContext
разрешить планирование заданий только в одном потоке (например, WPF, WebAPI и т. д.). Приведенный ниже код воспроизводит проблему в консольном приложении, где нет явного SynchronizationContext
набор и задачи планируются в пуле потоков.
Я знаю, что "решением" этой проблемы является " асинхронность полностью ". На самом деле мы можем не знать, что где-то глубоко внутри разработчик SyncMethod
подавляет асинхронность, ожидая ее блокирующим способом, освобождая от таких проблем (даже если он может добиться цели с заменой SynchronizationContext
чтобы это не зашло в тупик хотя бы).
Каковы ваши предложения по решению такой проблемы, когда "асинхронность полностью" не вариант? Есть ли что-то еще, а не очевидное "не порождай столько задач одновременно"?
void Main()
{
List<Task> tasks = new List<Task>();
for (int i = 0; i < 60; i++)
tasks.Add(Task.Run(() => SyncMethod()));
bool exit = false;
Task.WhenAll(tasks.ToArray()).ContinueWith(t => exit = true);
while (!exit)
{
Print($"Thread count: {Process.GetCurrentProcess().Threads.Count}");
Thread.Sleep(1000);
}
}
void SyncMethod()
{
SomethingAsync().Wait();
}
async Task SomethingAsync()
{
await Task.Delay(1);
await Task.Delay(1); // extra puzzle -- why commenting one of these Delay will partially resolve the issue?
Print("async done");
}
void Print(object obj)
{
$"[{Thread.CurrentThread.ManagedThreadId}] {DateTime.Now} - {obj}".Dump();
}
Вот вывод. Обратите внимание, что все асинхронные продолжения застряли почти на минуту, а затем неожиданно продолжили выполнение.
[12] 30.01.2018 23:34:36 - Количество тем: 18 [12] 30.01.2018 23:34:37 - Количество тем: 32 [12] 30.01.2018 23:34:38 - Количество тем: 33 - НАЧИНАЕТСЯ РЕЗЕРВНЫЙ БАССЕЙН... [12] 30.01.2018 23:35:18 - Количество тем: 70 [12] 30.01.2018 23:35:19 - Количество тем: 71 [12] 30.01.2018 23:35:20 - Количество тем: 72 - ДО ВСЕХ РАСПИСАННЫХ ЗАДАЧ МОЖНО УСТАНОВИТЬ [8] 30.01.2018 23:35:20 - асинхронно выполнено - ПОЧЕМУ МИНУТУ ПОСЛЕ НАЧАЛА [8] 30.01.2018 23:35:20 - асинхронно выполнено - ПРОДОЛЖЕНИЯ НАЧИНАЮТСЯ... [61] 30.01.2018 23:35:20 - асинхронность выполнена [10] 30.01.2018 23:35:20 - асинхронность выполнена
0 ответов
Отвечая на исходный вопрос:
Каковы ваши предложения по решению такой проблемы, когда "полная асинхронность" не подходит? Есть ли что-то еще, кроме очевидного "не порождать столько задач одновременно"?
Ни в коем случае не решение основной причины, а количественное средство - мы можем настроить пул потоков, используяSetMinThreads
увеличение количества потоков, которые будут созданы без задержки (так что это будет быстрее, чем обычная "скорость впрыска", которая в моей настройке составляет 1 поток пула потоков в секунду). Как это работает в данной настройке, очень просто. В основном мы тратим потоки пула потоков, пока пул не станет достаточно большим, чтобы начать выполнять продолжения. Если мы начнем с достаточно большого пула, мы в основном исключаем период времени, в течение которого мы просто ограничиваемся искусственной "скоростью внедрения", которая пытается удерживать количество потоков на низком уровне (что имеет смысл, поскольку пул потоков предназначен для выполнения задач, связанных с ЦП. вместо блокировки ожидания асинхронной операции).
Я также должен оставить предупреждение.
По умолчанию минимальное количество потоков устанавливается равным количеству процессоров в системе. Вы можете использовать метод SetMinThreads, чтобы увеличить минимальное количество потоков. Однако необоснованное увеличение этих значений может вызвать проблемы с производительностью. Если одновременно запускается слишком много задач, все они могут показаться медленными. В большинстве случаев пул потоков будет работать лучше со своим собственным алгоритмом распределения потоков. Уменьшение минимального количества процессоров до меньшего, чем количество процессоров, также может снизить производительность.
Существует также интересная проблема, когда Microsoft рекомендует увеличить "минимальное количество потоков" для ASP.NET в качестве улучшения производительности / надежности в некоторых сценариях.
Интересно, что проблема, описанная в вопросе, не является чисто мнимой. Это реально. Это происходит с известным и широко признанным программным обеспечением. Пример из опыта - Identity Server 3.
https://github.com/IdentityServer/IdentityServer3.EntityFramework/issues/101
Реализация, имеющая это предостережение (нам пришлось переписать его, чтобы обойти проблему для нашего производственного сценария):
Еще одна статья, в которой подробно объясняется проблема.
Что касается странного поведения одиноких Task.Delay
где некоторые асинхронные вызовы завершаются с каждым новым внедренным потоком пула потоков. Похоже, это вызвано тем, что выполнение продолжения встраивается вместе со способомTask.Delay
а также Timer
реализованы. См. Этот стек вызовов, он показывает, что вновь созданный поток пула потоков делает некоторую дополнительную магию для таймеров.NET при его создании перед обработкой очереди пула потоков (см.System.Threading.TimerQueue.AppDomainTimerCallback
).
в AsynchronySamples.StrangeTimer.Program.d__2.MoveNext() в System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.InvokeMoveNext(StateMachine объекта) в System.Threading.ExecutionContext.RunInternal(контекст выполнения ExecutionContext, обратный вызов ContextCallback, состояние объекта, логическое значение preserveSyncCtx) в System.Threading.ExecutionContext.Run(контекст выполнения ExecutionContext, обратный вызов ContextCallback, состояние объекта, логическое значение preserveSyncCtx) в System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.Run() в System.Runtime.CompilerServices.AsyncMethodBuilderCore.c__DisplayClass4_0.b__0() в System.Runtime.CompilerServices.AsyncMethodBuilderCore.ContinuationWrapper.Invoke() в System.Runtime.CompilerServices.TaskAwaiter.c__DisplayClass11_0.b__0() в System.Runtime.CompilerServices.AsyncMethodBuilderCore.ContinuationWrapper.Invoke() в System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(действие действия, логическое значение allowInlining, Task и currentTask) в System.Threading.Tasks.Task.FinishContinuations() в System.Threading.Tasks.Task.FinishStageThree() в System.Threading.Tasks.Task`1.TrySetResult(результат TResult) в System.Threading.Tasks.Task.DelayPromise.Complete() в System.Threading.Tasks.Task.c.b__274_1(состояние объекта) в System.Threading.TimerQueueTimer.CallCallbackInContext(состояние объекта) в System.Threading.ExecutionContext.RunInternal(контекст выполнения ExecutionContext, обратный вызов ContextCallback, состояние объекта, логическое значение preserveSyncCtx) в System.Threading.ExecutionContext.Run(контекст выполнения ExecutionContext, обратный вызов ContextCallback, состояние объекта, логическое значение preserveSyncCtx) в System.Threading.TimerQueueTimer.CallCallback() в System.Threading.TimerQueueTimer.Fire() в System.Threading.TimerQueue.FireNextTimers() в System.Threading.TimerQueue.AppDomainTimerCallback(идентификатор Int32) [Переход от собственного к управляемому] в kernel32.dll!74e86359() at kernel32.dll![Фреймы ниже могут быть неправильными и / или отсутствующими, символы для kernel32.dll не загружены] в ntdll.dll!77057b74() в ntdll.dll!77057b44()