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, чтобы увеличить минимальное количество потоков. Однако необоснованное увеличение этих значений может вызвать проблемы с производительностью. Если одновременно запускается слишком много задач, все они могут показаться медленными. В большинстве случаев пул потоков будет работать лучше со своим собственным алгоритмом распределения потоков. Уменьшение минимального количества процессоров до меньшего, чем количество процессоров, также может снизить производительность.

https://docs.microsoft.com/en-us/dotnet/api/system.threading.threadpool.setminthreads?view=netframework-4.8

Существует также интересная проблема, когда Microsoft рекомендует увеличить "минимальное количество потоков" для ASP.NET в качестве улучшения производительности / надежности в некоторых сценариях.

https://support.microsoft.com/en-us/help/821268/contention-poor-performance-and-deadlocks-when-you-make-calls-to-web-s

Интересно, что проблема, описанная в вопросе, не является чисто мнимой. Это реально. Это происходит с известным и широко признанным программным обеспечением. Пример из опыта - Identity Server 3.

https://github.com/IdentityServer/IdentityServer3.EntityFramework/issues/101

Реализация, имеющая это предостережение (нам пришлось переписать его, чтобы обойти проблему для нашего производственного сценария):

https://github.com/IdentityServer/IdentityServer3.EntityFramework/blob/master/Source/Core.EntityFramework/Serialization/ClientConverter.cs

Еще одна статья, в которой подробно объясняется проблема.

https://blogs.msdn.microsoft.com/vancem/2018/10/16/diagnosing-net-core-threadpool-starvation-with-perfview-why-my-service-is-not-saturating-all-cores-or-seems-to-stall/

Что касается странного поведения одиноких 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()  

Другие вопросы по тегам