如何使死锁安全的异步库方法

时间:2020-07-22 22:22:11

标签: c# .net async-await task-parallel-library

我发布了一个开源.Net库。有没有一种死锁安全的方法来公开需要执行长时间运行的任务的方法,例如DNS查找?

在下面的示例中,f1f2表示可从nuget包访问的库函数。 Task.Delay调用表示一些长期运行的网络任务,例如DNS查找。根据调用者的需求,f1f2这些库方法可能被称为即发即忘,或者它们可能要等待完成。

我的问题是如何编写一个库方法,以使调用者没有死锁的风险?

通话旁的注释显示哪些死锁。 f1是最好的选择,但呼叫者必须意识到对.ConfigureAwait(false)的需求。

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Deadlock
{
    class Program
    {
        private static Form _form;

        public static async Task Main()
        {
            _form = new Form();

            Func<int, Task> f1 = (i) =>
            {
                Console.WriteLine($"{DateTime.Now.Second}: f1 {i}");

                return Task.Delay(1000)
                .ContinueWith(t => Console.WriteLine($"{DateTime.Now.Second}: f1 {i} finished"));
            };

            Func<int, Task> f2 = async (i) =>
            {
                Console.WriteLine($"{DateTime.Now.Second}: f2 {i}");

                await Task.Delay(1000)
                .ContinueWith(t => Console.WriteLine($"{DateTime.Now.Second}: f2 {i} finished"));
            };

            var syncCtx = SynchronizationContext.Current;
            Console.WriteLine($"sync ctx ? {syncCtx != null}");

            for (int i = 0; i < 3; i++)
            {
                _ = f1(i);                         // OK.
                //_ = f2(i);                         // OK.
                //await f1(i);                       // Deadlock.
                //await f1(i).ConfigureAwait(false); // OK.
                //await f2(i);                       // Deadlock.
                //await f2(i).ConfigureAwait(false); // Deadlock.
                Thread.Sleep(1000);
            }

            Console.WriteLine($"{DateTime.Now.Second}: Finished");
            Console.ReadLine();
        }
    }
}

2 个答案:

答案 0 :(得分:3)

是否有一种死锁安全的方法来公开需要执行长时间运行的任务的方法,例如DNS查找?

是:

  1. 使用await代替ContinueWith
  2. ConfigureAwait(false)使用await。有可用的分析器可以强制执行此操作,并确保您不会误会一个。

在下面的示例中

该示例代码存在缺陷,因为它使用的UI SynchronizationContext不执行消息泵送。您应该创建一个实际的WinForms / WPF应用程序来运行这些测试,然后您会发现死锁行为不同于该问题中的示例代码。

答案 1 :(得分:-2)

对于该问题的评论暗示,没有一种方法可以使库eliminate the chance of deadlocks用于调用其方法的应用程序。调用库方法不是直接的问题,尽管如果库在内部return Promise.resolve()上不使用.ConfigureAwait(false)则可以。

更新: 按照上述斯蒂芬的回答,死锁可能是安全的,只是不要在所有内部库awaits调用上忘记ConfigureAwait(false)

示例代码中出现死锁的原因是await(使用Windows Forms变量人为引入)中的顶部项SynchronizationContext's正在等待第二项。 proper explanation

更新: 使用相同调用的Windows Forms应用程序不会死锁。这意味着我仍然不完全理解控制台应用程序死锁的原因。创建同步上下文之后,第一个非Dispatcher Queue连续可能会始终阻止等待消息泵对其进行调用。继续前进。

原始ConfigureAwait(false)是红色鲱鱼。 ContinueWith关键字会导致await始终在幕后使用,因此ContinueWithf1是等效的,并且也由对原始问题的评论所暗示。

更新:在我看来,f2在某些情况下仍具有优势:它不涉及同步上下文开销,可以使用{{1 }}表达式而不是ContinueWith,并且根据错误处理要求,可以更简洁地链接多个if调用。