理解C#中的异步/等待

83

我开始学习 C# 5.0 中的异步/等待,并且完全不理解它。我不明白它如何用于并行处理。我尝试了以下非常基本的程序:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Task task1 = Task1();
            Task task2 = Task2();

            Task.WaitAll(task1, task2);

            Debug.WriteLine("Finished main method");
        }

        public static async Task Task1()
        {
            await new Task(() => Thread.Sleep(TimeSpan.FromSeconds(5)));
            Debug.WriteLine("Finished Task1");
        }

        public static async Task Task2()
        {
            await new Task(() => Thread.Sleep(TimeSpan.FromSeconds(10)));
            Debug.WriteLine("Finished Task2");
        }

    }
}

这个程序在调用Task.WaitAll()时被阻塞,永远无法完成,但我不明白为什么。我确定我只是漏掉了一些简单的东西或者没有正确理解它的运作方式,而现有的博客或MSDN文章都没有帮助到我。


5
使用 await Task.Delay(...); 替代 await new Task.... - David Heffernan
1
https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/ 是一个非常好的介绍。 - David Thielen
5个回答

75

我建议您先阅读我的入门介绍async/await,然后再查看微软官方文档中关于TAP的说明

正如我在入门博客文章中提到的那样,有几个Task成员是TPL的遗留物,在纯async代码中没有用处。应该使用Task.Run(或TaskFactory.StartNew)来替换new TaskTask.Start。同样地,Thread.Sleep应该被替换为Task.Delay

最后,我建议您不要使用Task.WaitAll;您的控制台应用程序只需等待一个单独的Task,该任务使用Task.WhenAll。通过所有这些更改,您的代码将如下所示:

class Program
{
    static void Main(string[] args)
    {
        MainAsync().Wait();
    }

    public static async Task MainAsync()
    {
        Task task1 = Task1();
        Task task2 = Task2();

        await Task.WhenAll(task1, task2);

        Debug.WriteLine("Finished main method");
    }

    public static async Task Task1()
    {
        await Task.Delay(5000);
        Debug.WriteLine("Finished Task1");
    }

    public static async Task Task2()
    {
        await Task.Delay(10000);
        Debug.WriteLine("Finished Task2");
    }
}

谢谢您的回复,Stephen。您能否简要说明一下为什么使用await Task.WhenAll(...)比Task.WaitAll()更可取?此外,您能否提供任何文档链接,解释诸如WPF和Windows Runtime之类的API如何知道如何异步执行事件处理程序? - Alex Marshall
1
阻塞任务可能会导致死锁。在您的特定情况下,这是可以接受的,因为控制台Main方法是“不要阻塞”的指南的例外。但我更喜欢将异常代码(使用Wait)与任何逻辑分开,将其移动到MainAsync中。如果/当您将此代码复制/粘贴到GUI或ASP.NET应用程序中时,这样做就会减少死锁的机会。 - Stephen Cleary
2
异步事件处理程序利用当前的“同步上下文”。WPF(我相信WinRT也是如此)不必做任何特殊处理;它们现有的“同步上下文”已足以使异步事件处理程序正确运行。在我的介绍文章中,我解释了上下文捕获/恢复,并且我还有一篇MSDN文章,详细介绍了“同步上下文”,如果你感兴趣的话。 - Stephen Cleary

17

了解C#中的Task、async和await

C# Task

Task类是一个异步任务包装器。Thread.Sleep(1000)可以暂停线程运行1秒钟,而Task.Delay(1000)则不会停止当前工作。请参见代码:

public static void Main(string[] args){
    TaskTest();
}
private static void TaskTest(){
     Task.Delay(5000);
     System.Console.WriteLine("task done");
}

当运行时,"任务完成"将立即显示。因此,我可以假设来自Task的每个方法都应该是异步的。如果我用Task.Run(()=> TaskTest())替换TaskTest(),直到我在Run方法后附加一个Console.ReadLine(),任务完成才会完全显示出来。

在内部,Task类表示状态机中的线程状态。状态机中的每个状态都有几个状态,例如开始、延迟、取消和停止。

async和await

现在,你可能想知道如果所有的任务都是异步的,那么Task.Delay的目的是什么?接下来,让我们通过使用async和await真正地延迟运行线程。

public static void Main(string[] args){
     TaskTest();
     System.Console.WriteLine("main thread is not blocked");
     Console.ReadLine();
}
private static async void TaskTest(){
     await Task.Delay(5000);
     System.Console.WriteLine("task done");
}

异步方法告诉调用方,我是一个异步方法,请不要等待我。在 TaskTest() 内部使用 await 可以请求等待异步任务完成。现在,在运行之后,程序将等待 5 秒钟来显示任务完成文本。

取消任务

由于任务是一个状态机,因此必须有一种方法可以在任务运行时取消任务。

static CancellationTokenSource tokenSource = new CancellationTokenSource();
public static void Main(string[] args){
    TaskTest();
    System.Console.WriteLine("main thread is not blocked");
    var input=Console.ReadLine();
    if(input=="stop"){
          tokenSource.Cancel();
          System.Console.WriteLine("task stopped");
     }
     Console.ReadLine();
}
private static async void TaskTest(){
     try{
          await Task.Delay(5000,tokenSource.Token);
     }catch(TaskCanceledException e){
          //cancel task will throw out a exception, just catch it, do nothing.
     }
     System.Console.WriteLine("task done");
}

现在,当程序正在运行时,您可以输入“stop”来取消延迟任务。


13

你的任务从来没有完成,因为它们从未开始运行。

我会使用 Task.Factory.StartNew 来创建并启动一个任务。

public static async Task Task1()
{
  await Task.Factory.StartNew(() => Thread.Sleep(TimeSpan.FromSeconds(5)));
  Debug.WriteLine("Finished Task1");
}

public static async Task Task2()
{
  await Task.Factory.StartNew(() => Thread.Sleep(TimeSpan.FromSeconds(10)));
  Debug.WriteLine("Finished Task2");
}

另外提一句,如果你只是想在异步方法中暂停,没有必要阻塞整个线程,只需使用 Task.Delay

public static async Task Task1()
{
  await Task.Delay(TimeSpan.FromSeconds(5));
  Debug.WriteLine("Finished Task1");
}

public static async Task Task2()
{
  await Task.Delay(TimeSpan.FromSeconds(10));
  Debug.WriteLine("Finished Task2");
}

1
为什么要创建新任务? - David Heffernan
1
@DavidHeffernan还没有来得及在我的答案中添加这个,但我猜想Alex可能有更复杂的想法。 - MerickOWA
@MerickOWA 是的,你说得对,我确实有更复杂的想法。我想模拟在IO上阻塞并且需要很长时间的线程,而不是纯粹受CPU限制的线程。 - Alex Marshall

8

1
static void Main(string[] args)
{
    if (Thread.CurrentThread.Name == null)
        Thread.CurrentThread.Name = "Main";
    Console.WriteLine(Thread.CurrentThread.Name + "1");

    TaskTest();

    Console.WriteLine(Thread.CurrentThread.Name + "2");
    Console.ReadLine();
}


private async static void TaskTest()
{
    Console.WriteLine(Thread.CurrentThread.Name + "3");

    await Task.Delay(2000);
    if (Thread.CurrentThread.Name == null)
        Thread.CurrentThread.Name = "FirstTask";
    Console.WriteLine(Thread.CurrentThread.Name + "4");

    await Task.Delay(2000);
    if (Thread.CurrentThread.Name == null)
        Thread.CurrentThread.Name = "SecondTask";
    Console.WriteLine(Thread.CurrentThread.Name + "5");
}

如果您运行此程序,您将看到await将使用不同的线程。输出:
Main1
Main3
Main2
FirstTask4 // 2 seconds delay
SecondTask5 // 4 seconds delay

但是如果我们删除两个await关键字,您将了解到仅有async并不能做太多事情。输出:
Main1
Main3
Main4
Main5
Main2

"await将启动新线程": 你怎么知道这个线程是新的,它刚刚被启动了?它可能是一个可重用的ThreadPool线程,很久以前就已经启动了。你可以查询Thread.IsThreadPoolThread属性来找出答案。 - Theodor Zoulias
1
@TheodorZoulias 我的意思是“使用不同的线程”,但是我没有测试它是来自池中的新线程还是重用的旧线程,所以我不知道它是新的还是旧的。我编辑了我的帖子,改为“使用不同的线程”。 - milos

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接