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

问题描述 投票:0回答:3

任何人都可以解释为什么这个代码只是在WhenAll触发后才会达到死胡同?

主要代码:

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

UPDATE

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

如果我将“DoNothing”更改为执行睡眠的异步任务,那么我被告知我需要await运算符。这意味着我需要编写另一个async方法等待。那么在运营商方面结束这一系列呼叫的最佳方式是什么?

有人可以展示如何实现上述目标的最佳实践示例吗?

c# async-await
3个回答
4
投票

你永远不会在DoNothing中开始你的任务。

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

分离任务创建和执行

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


0
投票

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

public async Task DoNothing() { }

上面实际上什么也没做,将返回一个处于完成状态的Task,可以等待。

您当前正在执行此操作的方式,即创建了Task,但它从未启动或设置为Completed,因此等待它将永远锁定程序。


0
投票

为了补充已经给出的答案和评论,我想展示一个代码示例,以我在测试中的方式工作。它显示了执行流程+ for info,特定时间执行的托管线程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
© www.soinside.com 2019 - 2024. All rights reserved.