Task.Start/Wait和Async/Await有什么区别?

221

我可能有所遗漏,但是做以下两种操作有什么区别:

public void MyMethod()
{
  Task t = Task.Factory.StartNew(DoSomethingThatTakesTime);
  t.Wait();
  UpdateLabelToSayItsComplete();
}

public async void MyMethod()
{
  var result = Task.Factory.StartNew(DoSomethingThatTakesTime);
  await result;
  UpdateLabelToSayItsComplete();
}

private void DoSomethingThatTakesTime()
{
  Thread.Sleep(10000);
}
6个回答

418

也许我漏掉了什么

确实是的。

使用 Task.Waitawait task 有什么区别?

你在餐厅向服务员点了一份午餐。在你下单后,一个朋友走进来坐到你旁边并开始谈话。现在你有两个选择。你可以忽略你的朋友直到任务完成 - 在等待期间不做其他事情。或者你可以和你的朋友交流,在你的朋友停止谈话时服务员会给你上汤。

Task.Wait 阻塞线程直到任务完成 - 在等待期间你忽略你的朋友。await则继续处理消息队列,当任务完成时,它会将一条消息排队,以便在“await”之后继续执行。你和你的朋友聊天,在对话中有空隙时餐厅服务员会给你上汤。


6
不是这样的。如果等待一个需要 10 毫秒才能完成的 “Task” 实际上会在您的线程上执行一个长达 10 小时的“Task”,从而让您被阻塞了整整 10 小时,您会喜欢吗? - svick
73
“await” 运算符除了评估其操作数并立即返回一个任务给当前调用者外,不会执行其他任何操作。人们认为异步只能通过将工作转移到线程上才能实现,但这是错误的。你可以在烤面包的时候煮早饭和看报纸,而不需要雇佣一个厨师来看着烤面包。人们说,烤面包机里一定有一个线程-一个工作者-隐藏在里面,但我向你保证,如果你看看你的烤面包机,里面没有一个小家伙在看着烤面包。 - Eric Lippert
12
“那么,谁在进行这项工作呢?”你问道。也许是另一个线程正在进行这项工作,并且该线程已被分配给CPU,因此实际上正在进行工作。也许工作是由硬件完成的,根本就没有线程。但是,你肯定会问,硬件中一定会有一些线程吧。不是这样的。硬件存在于线程的下面。不需要任何线程!你可能会从读斯蒂芬·克利尔里的文章“没有线程”中受益。 - Eric Lippert
8
@StrugglingCoder: 现在问题来了,假设正在进行异步工作,并且没有硬件,也没有其他线程。这可能吗?嗯,假设你等待的东西排队一系列窗口消息,每个消息都会做一点点工作。现在会发生什么?你将控制权返回给消息循环,它开始从队列中拉出消息,每次都会做一点点工作,最后完成的工作是“执行任务的继续”。没有额外的线程! - Eric Lippert
9
@StrugglingCoder:现在,想一想我刚才说的话。 你已经知道这就是Windows的工作方式。你执行一系列的鼠标移动、按钮点击等操作。消息被排队等待处理,按顺序依次处理每个消息,每个消息都会引起少量的工作量。当所有工作完成后,系统继续运行。单线程异步编程本质上与你已经习惯的相同:将大任务分解成小任务,将它们排队,按顺序执行所有小任务。其中一些执行会导致其他任务排队等待处理,生活就这样继续下去。一条线程! - Eric Lippert
显示剩余16条评论

126
为了展示Eric的回答,这里提供了一些代码:
public void ButtonClick(object sender, EventArgs e)
{
  Task t = new Task.Factory.StartNew(DoSomethingThatTakesTime);
  t.Wait();  
  //If you press Button2 now you won't see anything in the console 
  //until this task is complete and then the label will be updated!
  UpdateLabelToSayItsComplete();
}

public async void ButtonClick(object sender, EventArgs e)
{
  var result = Task.Factory.StartNew(DoSomethingThatTakesTime);
  await result;
  //If you press Button2 now you will see stuff in the console and 
  //when the long method returns it will update the label!
  UpdateLabelToSayItsComplete();
}

public void Button_2_Click(object sender, EventArgs e)
{
  Console.WriteLine("Button 2 Clicked");
}

private void DoSomethingThatTakesTime()
{
  Thread.Sleep(10000);
}

30
代码加1分(运行一次胜过读一百遍)。但是短语“//如果你现在按Button2,直到此任务完成并更新标签,控制台将不会显示任何内容!”是具有误导性的。在单击按钮处理程序ButtonClick()中包含t.Wait();时,无法通过按任何东西来查看控制台上的内容或更新标签,因为GUI被冻结并且无响应,也就是说,在等待任务完成之前,任何与GUI的交互都会丢失 - Gennady Vanin Геннадий Ванин
2
我猜Eric认为你已经基本了解了Task API。我看着那段代码,对自己说:“是的,t.Wait会在主线程上阻塞,直到任务完成。” - The Muffin Man

54
这个例子非常清楚地展示了差异。使用async/await,调用线程不会阻塞并继续执行。
static void Main(string[] args)
{
    WriteOutput("Program Begin");
    // DoAsTask();
    DoAsAsync();
    WriteOutput("Program End");
    Console.ReadLine();
}

static void DoAsTask()
{
    WriteOutput("1 - Starting");
    var t = Task.Factory.StartNew<int>(DoSomethingThatTakesTime);
    WriteOutput("2 - Task started");
    t.Wait();
    WriteOutput("3 - Task completed with result: " + t.Result);
}

static async Task DoAsAsync()
{
    WriteOutput("1 - Starting");
    var t = Task.Factory.StartNew<int>(DoSomethingThatTakesTime);
    WriteOutput("2 - Task started");
    var result = await t;
    WriteOutput("3 - Task completed with result: " + result);
}

static int DoSomethingThatTakesTime()
{
    WriteOutput("A - Started something");
    Thread.Sleep(1000);
    WriteOutput("B - Completed something");
    return 123;
}

static void WriteOutput(string message)
{
    Console.WriteLine("[{0}] {1}", Thread.CurrentThread.ManagedThreadId, message);
}

DoAsTask 输出:

[1] 程序开始
[1] 1 - 正在启动
[1] 2 - 任务已开始
[3] A - 开始了某些操作
[3] B - 完成了某些操作
[1] 3 - 任务已完成,结果为:123
[1] 程序结束

DoAsAsync 输出:

[1] 程序开始
[1] 1 - 正在启动
[1] 2 - 任务已开始
[3] A - 开始了某些操作
[1] 程序结束
[3] B - 完成了某些操作
[3] 3 - 任务已完成,结果为:123

更新:通过输出线程 ID 来改进示例。


4
但是,如果我使用:new Task(DoAsTask).Start();而不是DoAsAsync();我可以获得相同的功能,那么等待的好处在哪里呢? - omriman12
1
通过您的提案,任务的结果必须在其他地方进行评估,可能是另一种方法或lambda表达式。async-await使异步代码更易于跟踪。它只是语法增强器。 - Mas
@Mas,我不明白为什么程序结束在A - Started something之后。据我理解,当涉及到await关键字时,进程应立即转到主上下文,然后返回。 - user6156963
根据我的理解,Task.Factory.StartNew会启动一个新线程来运行DoSomethingThatTakesTime。因此,无法保证Program End或A - Started Something哪个先执行。 - RiaanDP
@JimmyJimm:我已经更新了示例代码以显示线程ID。正如你所看到的,“程序结束”和“A-开始做某事”正在不同的线程上运行。因此,实际的顺序是不确定的。 - Mas
@Mas,我相信你的代码有一个bug,你应该在DoAsAsync上也调用await - 这样它们应该会给出相同的输出。这段代码能编译吗?我原以为异步的Main也需要被使用。 - undefined

14

Wait()会使潜在的异步代码以同步方式运行,而await不会。

例如,您有一个asp.net Web应用程序。UserA调用/getUser/1端点。asp.net应用程序池将从线程池中选择一个线程(Thread1),这个线程将进行http调用。如果您使用Wait(),则此线程将被阻塞,直到http调用解析为止。当它等待时,如果UserB调用/getUser/2,则应用程序池将需要提供另一个线程(Thread2)再次进行http调用。您无缘无故地创建了另一个线程,因为您不能使用被Wait()阻塞的Thread1。

如果您在Thread1上使用await,则SyncContext将管理Thread1和http调用之间的同步。简单地说,一旦http调用完成,它就会通知您。同时,如果UserB调用/getUser/2,那么您将再次使用Thread1进行http调用,因为一旦await命中,它就被释放了。然后可以再次使用另一个请求。一旦http调用完成(用户1或用户2),Thread1便可以获取结果并返回给调用方(客户端)。Thread1被用于多个任务。


10

在这个例子中,实际上没有太多作用。如果你正在等待一个在不同线程返回的任务(比如WCF调用),或是放弃控制权给操作系统(比如文件IO)的任务,使用await会减少系统资源的使用,因为它不会阻塞线程。


3
在上面的示例中,您可以使用“TaskCreationOptions.HideScheduler”并大幅修改“DoAsTask”方法。该方法本身不是异步的,就像使用“DoAsAsync”时一样,因为它返回一个“Task”值并标记为“async”,通过多种组合,这正是它给我带来的与使用“async/await”完全相同的结果:
static Task DoAsTask()
{
    WriteOutput("1 - Starting");
    var t = Task.Factory.StartNew<int>(DoSomethingThatTakesTime, TaskCreationOptions.HideScheduler); //<-- HideScheduler do the magic

    TaskCompletionSource<int> tsc = new TaskCompletionSource<int>();
    t.ContinueWith(tsk => tsc.TrySetResult(tsk.Result)); //<-- Set the result to the created Task

    WriteOutput("2 - Task started");

    tsc.Task.ContinueWith(tsk => WriteOutput("3 - Task completed with result: " + tsk.Result)); //<-- Complete the Task
    return tsc.Task;
}

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