为什么此控制台应用程序中的异步等待模式会导致死锁?

时间:2019-04-30 16:56:40

标签: c# async-await

有人能解释一下,为什么在AfterAll触发后此代码只会陷入死胡同吗?

主要代码:

class AsyncTests
{
    public async void Start()
    {
        Console.WriteLine($"Thread:{Thread.CurrentThread.ManagedThreadId} - starting whole process, calling await DoWork1()");
        await Task.WhenAll(DoWork1(), DoWork2());
        Console.WriteLine($"Thread:{Thread.CurrentThread.ManagedThreadId} - finished awaiting DoWork1 and DoWork2");
    }

    public async Task DoWork1()
    {
        Console.WriteLine($"Thread:{Thread.CurrentThread.ManagedThreadId} - starting DoWork1");
        await DoNothing();
        Console.WriteLine($"Thread:{Thread.CurrentThread.ManagedThreadId} - finished DoWork1");
    }

    public async Task DoWork2()
    {
        Console.WriteLine($"Thread:{Thread.CurrentThread.ManagedThreadId} - starting DoWork2");
        await DoNothing();
        Console.WriteLine($"Thread:{Thread.CurrentThread.ManagedThreadId} - finished DoWork2");
    }

    public Task DoNothing() { return new Task(() => { /* do nothing */ }); }
}

程序控制代码:

    static void Main(string[] args)
    {            
        var x = new AsyncTests();
        Console.WriteLine($"Thread:{Thread.CurrentThread.ManagedThreadId} - Main ... calling Start()");
        Task.Run(() => x.Start());
        Console.WriteLine($"Thread:{Thread.CurrentThread.ManagedThreadId} - Main ... start is running");
        Console.ReadKey();
    }

输出:

Thread:1 - Main ... calling Start()
Thread:1 - Main ... start is running
Thread:4 - starting whole process, calling await DoWork1()
Thread:4 - starting DoWork1
Thread:4 - starting DoWork2

更新

为了更清楚一点,让我们对其进行更改,以便DoNothing实际调用Thread.Sleep(2000),我的目标是同时运行两个线程睡眠,并希望使用async / await模式来实现这一点。

如果我将“ DoNothing”更改为执行睡眠的异步任务,那么我被告知我需要await操作符。这意味着我需要编写另一个async方法来等待。那么从运营商的角度来看,结束通话链的最佳方法是什么?

有人可以举例说明如何实现上述目标吗?

3 个答案:

答案 0 :(得分:4)

您永远不会在NoNothing中开始您的任务。

https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task?view=netframework-4.8#remarks

分隔任务创建和执行

Task类还提供了初始化任务但不安排执行任务的构造函数。出于性能原因,Task.Run或TaskFactory.StartNew方法是创建和调度计算任务的首选机制,但是对于必须将创建和调度分开的情况,可以使用构造函数,然后调用Task.Start方法进行调度。该任务以便以后执行。

答案 1 :(得分:0)

让该语言为您完成任务,而不是创建要返回的任务。

public async Task DoNothing() { }

以上内容实际上不执行任何操作,并且会返回处于完成状态的Task,可以等待。

您目前正在执行的方式是创建任务,但它从未启动或设置为已完成,因此等待它会永远锁定程序。

答案 2 :(得分:0)

仅是为了补充已经给出的答案和注释,我想展示一个代码示例,其工作方式与测试中的预期目的相同。它显示信息的执行流+,特定时间执行的托管线程ID。

主要代码:

class AsyncTests
{
    public async Task StartAsync()
    {
        Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} - Thread:{Thread.CurrentThread.ManagedThreadId} - starting whole process, calling await DoWork1Async()");
        await Task.WhenAll(DoWork1Async(), DoWork2Async());
        Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} - Thread:{Thread.CurrentThread.ManagedThreadId} - finished awaiting DoWork1Async and DoWork2Async");
    }

    public async Task DoWork1Async()
    {
        Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} - Thread:{Thread.CurrentThread.ManagedThreadId} - starting DoWork1Async");
        await Sleep();
        Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} - Thread:{Thread.CurrentThread.ManagedThreadId} - finished DoWork1Async");
    }

    public async Task DoWork2Async()
    {
        Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} - Thread:{Thread.CurrentThread.ManagedThreadId} - starting DoWork2Async");
        await Sleep();
        Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} - Thread:{Thread.CurrentThread.ManagedThreadId} - finished DoWork2Async");
    }        

    public async Task Sleep()
    {
        await Task.Delay(2000);
    }
}

调用代码(请注意,要使其异步运行,我必须省去await运算符,该运算符在consider applying the 'await' operator方法调用中引发警告StartAsync()

static void Main(string[] args)
{            
    var x = new AsyncTests();
    Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} - {Thread.CurrentThread.ManagedThreadId} - Main ... calling Start()");
    x.StartAsync();
    Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} - {Thread.CurrentThread.ManagedThreadId} - Main ... start is running");
    Console.ReadKey();
}

最后,输出-如预期的那样,显示代码执行/控制返回到真正异步操作所需的位置。正如预期的那样,使用了两个不同的池线程来运行睡眠。

10:43:36.515 - 1 - Main ... calling Start()
10:43:36.546 - Thread:1 - starting whole process, calling await DoWork1Async()
10:43:36.547 - Thread:1 - starting DoWork1Async
10:43:36.561 - Thread:1 - starting DoWork2Async
10:43:36.562 - 1 - Main ... start is running
10:43:38.581 - Thread:4 - finished DoWork2Async
10:43:38.582 - Thread:5 - finished DoWork1Async
10:43:38.582 - Thread:5 - finished awaiting DoWork1Async and DoWork2Async