【问题标题】:Locking issue with LimitedConcurrencyLevelTaskScheduler and aync/awaitLimitedConcurrency 级别 TaskScheduler 和 async/await 的锁定问题
【发布时间】:2019-05-30 22:46:03
【问题描述】:

我很难理解这个简单程序中发生了什么。

在下面的示例中,我有一个任务工厂,它使用 ParallelExtensionsExtras 中的 LimitedConcurrencyLevelTask​​Scheduler,maxDegreeOfParallelism 设置为 2。

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

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

它似乎与 async/await 相关,因为如果我删除它并简单地在 DoWork 中执行 Task.Delay(2000).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);
        }
    }
}

提前感谢您的帮助

【问题讨论】:

  • 您可以放心地假定“LimitedConcurrency”就是它所说的那样。灵活的无限制任务调度程序以建设性的方式处理死锁错误,它确实将并发级别提高到超过最佳水平。就像 ThreadPool 和 Task 一样。使用 Debug > Windows > Threads 查看发生了什么。

标签: c# .net async-await task


【解决方案1】:

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

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

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

说明:

问题似乎是 Task.Delay(2000) 永远不会完成。

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

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

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

这似乎与 async/await 相关,因为如果我删除它并在 DoWork 中简单地执行 Task.Delay(2000).GetAwaiter().GetResult() ,它会完美运行。

是的;捕获上下文的是awaitTask.Delay 内部不捕获上下文,因此无需输入LimitedConcurrencyTaskScheduler 即可完成。

解决方案:

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

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

现在,您可以添加更多技巧,在任何地方使用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);
  }
}

【讨论】:

  • 感谢@Stephen 的回答并清楚地说明。我遇到这个问题的原因是我被困在异步 API (HttpClient) 和同步 API 之间,而我现在对此无能为力。
【解决方案2】:

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

【讨论】:

  • 不幸的是,这个例子不能像写的那样工作,TaskFactory.StartNew 没有接受 Func 的重载。 tasks 列表的类型为 IEnumerable<Task<Task>>。外部任务将在 DoWork 等待时完成并返回。该程序将在打印“Starting Work”两次后退出,并且不会等待内部任务完成。但是,如果您使用 Task.Unwrap() 则可以。
  • 大声笑,Cleary 先生带来了 300k 的代表,并提供了比我以往任何时候都更有说服力的答案,然后我让 @shf301 通过 Visual Studio 运行我的代码,并在定位时对包括警告在内的事情进行挑剔错误的sdk。翻录。
猜你喜欢
  • 1970-01-01
  • 2020-03-25
  • 2012-09-23
  • 2020-07-16
  • 2021-09-20
  • 1970-01-01
  • 1970-01-01
  • 2019-10-08
  • 1970-01-01
相关资源
最近更新 更多