Проблема с блокировкой в ​​LimitedConcurrencyLevelTaskScheduler и aync / await - PullRequest
0 голосов
/ 31 мая 2019

Я изо всех сил пытаюсь понять, что происходит в этой простой программе.

В приведенном ниже примере у меня есть фабрика задач, которая использует LimitedConcurrencyLevelTaskScheduler из ParallelExtensionsExtras с maxDegreeOfParallelism, установленным в 2.

Iзатем запустите 2 задачи, каждая из которых вызывает асинхронный метод (например, асинхронный Http-запрос), затем получает ожидающего и результат выполненной задачи.

Кажется, проблема в том, что Task.Delay(2000) никогда не завершается.Если я установлю maxDegreeOfParallelism на 3 (или больше), он завершится.Но с maxDegreeOfParallelism = 2 (или меньше) я предполагаю, что нет потока, доступного для выполнения задачи.Почему это так?

Похоже, это связано с async / await, поскольку, если я удаляю его и просто выполняю Task.Delay(2000).GetAwaiter().GetResult() в DoWork, он работает отлично.Использует ли async / await какой-либо планировщик задач родительской задачи или как он подключен?

using System;
using System.Linq;
using System.Threading.Tasks;
using System.Threading.Tasks.Schedulers;

namespace LimitedConcurrency
{
    class Program
    {
        static void Main(string[] args)
        {
            var test = new TaskSchedulerTest();
            test.Run();
        }
    }

    class TaskSchedulerTest
    {
        public void Run()
        {
            var scheduler = new LimitedConcurrencyLevelTaskScheduler(2);
            var taskFactory = new TaskFactory(scheduler);

            var tasks = Enumerable.Range(1, 2).Select(id => taskFactory.StartNew(() => DoWork(id)));
            Task.WaitAll(tasks.ToArray());
        }

        private void DoWork(int id)
        {
            Console.WriteLine($"Starting Work {id}");
            HttpClientGetAsync().GetAwaiter().GetResult();
            Console.WriteLine($"Finished Work {id}");
        }

        async Task HttpClientGetAsync()
        {
            await Task.Delay(2000);
        }
    }
}

Заранее благодарен за любую помощь

Ответы [ 2 ]

5 голосов
/ 31 мая 2019

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);
  }
}
0 голосов
/ 31 мая 2019

Я думаю, что вы столкнулись с тупиковой синхронизацией.Вы ожидаете завершения потока, ожидающего завершения потока.Никогда не случится.Если вы сделаете свой метод DoWork асинхронным, чтобы вы могли ожидать вызова HttpClientGetAsync (), и вы избежите тупика.

using MassTransit.Util;
using System;
using System.Linq;
using System.Threading.Tasks;
//using System.Threading.Tasks.Schedulers;

namespace LimitedConcurrency
{
    class Program
    {
        static void Main(string[] args)
        {
            var test = new TaskSchedulerTest();
            test.Run();
        }
    }

    class TaskSchedulerTest
    {
        public void Run()
        {
            var scheduler = new LimitedConcurrencyLevelTaskScheduler(2);
            var taskFactory = new TaskFactory(scheduler);

            var tasks = Enumerable.Range(1, 2).Select(id => taskFactory.StartNew(() => DoWork(id)));
            Task.WaitAll(tasks.ToArray());
        }

        private async Task DoWork(int id)
        {
            Console.WriteLine($"Starting Work {id}");
            await HttpClientGetAsync();
            Console.WriteLine($"Finished Work {id}");
        }

        async Task HttpClientGetAsync()
        {
            await Task.Delay(2000);
        }
    }
}

https://medium.com/rubrikkgroup/understanding-async-avoiding-deadlocks-e41f8f2c6f5d

TLDR никогда не вызывает .result, которыйЯ уверен .GetResult ();делал

...