C# LimitedConcurrentylevelTaskScheduler和aync/await的锁定问题

C# LimitedConcurrentylevelTaskScheduler和aync/await的锁定问题,c#,.net,async-await,task,C#,.net,Async Await,Task,我正在努力理解这个简单的程序中发生了什么 在下面的示例中,我有一个任务工厂,它使用ParallelExtensionsExtras中的LimitedConcurrenceyLevel任务调度器,并将maxDegreeOfParallelism设置为2 然后我启动两个任务,每个任务调用一个异步方法,例如异步Http请求,然后获取等待者和完成任务的结果 问题似乎在于任务。Delay2000永远不会完成。如果我将maxDegreeOfParallelism设置为3或更大,它将完成。但是如果maxDeg

我正在努力理解这个简单的程序中发生了什么

在下面的示例中,我有一个任务工厂,它使用ParallelExtensionsExtras中的LimitedConcurrenceyLevel任务调度器,并将maxDegreeOfParallelism设置为2

然后我启动两个任务,每个任务调用一个异步方法,例如异步Http请求,然后获取等待者和完成任务的结果

问题似乎在于任务。Delay2000永远不会完成。如果我将maxDegreeOfParallelism设置为3或更大,它将完成。但是如果maxDegreeOfParallelism=2或更小,我的猜测是没有线程可以完成任务。为什么呢

它似乎与async/await有关,因为如果我删除它并在DoWork中简单地执行Task.Delay2000.GetAwaiter.GetResult,它就可以完美地工作。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);
        }
    }
}

提前感谢您的帮助

我想您遇到了同步死锁。您正在等待一个线程完成,该线程正在等待您的线程完成。永远不会发生。如果将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);
        }
    }
}
TLDR从不调用.result,我确信是这样的.GetResult;正在执行

,并使用该操作恢复异步方法。此上下文是SynchronizationContext.Current,除非它为null,在这种情况下,它是TaskScheduler.Current

在本例中,await捕获用于执行DoWork的LimitedConcurrencyLevel TaskScheduler。因此,在启动Task.Delay两次之后,由于GetAwaiter.GetResult,这两个线程都被阻塞。当Task.Delay完成时,wait将HttpClientGetAsync方法的其余部分调度到其上下文中。但是,上下文不会运行它,因为它已经有2个线程

因此,在异步方法完成之前,在上下文中阻塞线程,但在上下文中有空闲线程之前,异步方法无法完成;因此出现了僵局。非常类似于,只是有n个线程而不是一个线程

澄清:

问题似乎在于任务。Delay2000永远不会完成

Task.Delay正在完成,但Wait无法继续执行异步方法

如果我将maxDegreeOfParallelism设置为3或更大,它将完成。但是如果maxDegreeOfParallelism=2或更小,我的猜测是没有线程可以完成任务。为什么呢

有很多线程可用。但是LimitedConcurrencyTaskScheduler一次只允许在其上下文中运行两个线程

它似乎与async/await有关,因为如果我删除它并在DoWork中简单地执行Task.Delay2000.GetAwaiter.GetResult,它就可以完美地工作

对,;正是等待捕获了上下文。延迟不会在内部捕获上下文,因此它可以在不需要输入LimitedConcurrencyTaskScheduler的情况下完成

解决方案:

任务调度器通常不能很好地处理异步代码。这是因为任务调度器是为并行任务而不是异步任务设计的。因此,它们仅在代码正在运行或被阻止时才适用。在这种情况下,LimitedConcurrencyLevel TaskScheduler只统计正在运行的代码;如果您有一个正在进行等待的方法,它将不计入该并发限制

因此,您的代码最终会遇到一种情况,即它具有异步同步反模式,这可能是因为有人试图避免Wait在有限并发任务调度器中无法按预期工作的问题。异步反模式上的同步导致了死锁问题

现在,您可以通过使用ConfigureWaitFalse everywhere并继续阻止异步代码来添加更多的攻击,或者您可以更好地修复它

更合适的修复方法是执行异步节流。彻底抛弃有限并发级别的TaskScheduler;限制并发性的任务调度器只能处理同步代码,并且您的代码是异步的。您可以使用信号量LIM执行异步节流,如下所示:

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);
  }
}

您可以放心地假设LimitedConcurrency就是它所说的。一个灵活的、不受限制的任务调度器以一种建设性的方式处理死锁错误,它确实将并发级别提高到了最佳水平。就像ThreadPool和TaskDo一样。使用Debug>Windows>Threads查看正在进行的操作。遗憾的是,此示例无法按编写的方式运行,TaskFactory.StartNew没有接受Func的重载。任务列表的类型为IEnumerable。外部任务将完成,并在工作完成后尽快返回。该程序将在打印开始工作两次后退出,不会等待内部任务完成。不过,如果您使用Task.Unwrap it can.Lol,Cleary先生带来了300k代表,并给出了比我更雄辩的答案,然后我让@shf301通过visual studio和ni运行我的代码
t瞄准错误的sdk时,点击包括警告在内的内容。瑞普。谢谢你@Stephen的回答,并清楚地说明了这一点。我遇到这个问题的原因是我被困在异步API HttpClient和同步API之间,目前对此我无能为力。