使用LimitedConcurrencyLevelTask​​Scheduler和aync / await锁定问题

时间:2019-05-30 22:46:03

标签: c# .net async-await task

我正在努力了解这个简单程序中发生的事情。

在以下示例中,我有一个任务工厂,该工厂使用来自ParallelExtensionsExtras的LimitedConcurrencyLevelTask​​Scheduler,并将maxDegreeOfParallelism设置为2。

然后我开始执行两个任务,每个任务都调用一个异步方法(例如,一个异步Http请求),然后获取等待者和完成任务的结果。

问题似乎是Task.Delay(2000)从未完成。如果我将maxDegreeOfParallelism设置为3(或更大),它将完成。但是在maxDegreeOfParallelism = 2(或更小)的情况下,我的猜测是没有可用的线程来完成任务。为什么会这样?

这似乎与async / await有关,因为如果我删除它并只是在Task.Delay(2000).GetAwaiter().GetResult()中执行DoWork,它就可以完美地工作。异步/等待是否以某种方式使用了父任务的任务计划程序,或者它是如何连接的?

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 个答案:

答案 0 :(得分:5)

await by default captures the current context,并使用它来恢复async方法。此上下文为SynchronizationContext.Current,除非它为null,在这种情况下为TaskScheduler.Current

在这种情况下,await正在捕获用于执行LimitedConcurrencyLevelTaskScheduler的{​​{1}}。因此,两次都启动DoWork之后,这两个线程都被阻塞了(由于Task.Delay)。 GetAwaiter().GetResult()完成后,Task.Delayawait方法的其余部分安排到其上下文中。但是,上下文将不会运行它,因为它已经有2个线程。

因此,您最终会在上下文中阻塞线程,直到它们的HttpClientGetAsync方法完成为止,但是async方法无法完成,直到上下文中有可用线程为止;因此陷入僵局。与standard "don't block on async code" style of deadlock非常相似,只是使用 n 个线程而不是一个。

说明:

  

问题似乎是Task.Delay(2000)从未完成。

async已完成,但是Task.Delay无法继续执行await方法。

  

如果将maxDegreeOfParallelism设置为3(或更大),则完成。但是在maxDegreeOfParallelism = 2(或更小)的情况下,我的猜测是没有可用的线程来完成任务。为什么会这样?

有很多可用的线程。但是async一次只能在其上下文中运行2个线程。

  

它似乎与async / await有关,因为如果我删除它并在DoWork中简单执行Task.Delay(2000).GetAwaiter()。GetResult(),它就可以正常工作。

是; LimitedConcurrencyTaskScheduler正在捕获上下文。 await不会在内部捕获上下文,因此无需输入Task.Delay就可以完成。

解决方案:

任务调度程序通常不能很好地与异步代码一起使用。这是因为任务计划程序是为并行任务而不是异步任务而设计的。因此,它们仅在代码运行(或被阻止)时适用。在这种情况下,LimitedConcurrencyTaskScheduler仅“计数”正在运行的代码。如果您有一种正在执行LimitedConcurrencyLevelTaskScheduler的方法,则不会根据该并发限制来“计数”。

因此,您的代码最终陷入具有同步异步模式的情况,这可能是因为有人试图避免await在有限的并发任务调度程序中无法按预期工作的问题。然后,这种基于异步的反模式导致了死锁问题。

现在,您可以通过在各处使用await来增加更多的黑客攻击,并继续阻塞异步代码,您可以更好地对其进行修复。

更合适的解决方法是进行异步限制。完全扔出ConfigureAwait(false);并发限制任务调度程序仅适用于同步代码,并且您的代码是异步的。您可以使用LimitedConcurrencyLevelTaskScheduler进行异步限制,如下所示:

SemaphoreSlim

答案 1 :(得分:0)

我认为您遇到同步死锁。您正在等待线程完成,而该线程正在等待线程完成。永远不会发生。如果您使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();。在做