使用await关键字和Task.Wait()方法在CPU绑定任务上的区别是什么?

3

这两者在机械上的区别是什么?

async void LongIOBoundWorkWithSomeCPUBoundWorkAsWellAsync()
{
    await Task.Run(CPUBoundWork);

    // Do IO bound work
    await DoIOAsync();
}

and

async void LongIOBoundWorkWithSomeCPUBoundWorkAsWellAsync()
{
    var cpuTask = Task.Run(CPUBoundWork);

    cpuTask.Wait();

    // Do IO bound work
    await DoIOAsync();
}

我知道在逻辑上,这两种方式的流程控制都是一样的。在这两种情况下,只有在 CPUBoundWork 任务完成执行后才会调用 DoIOAsync 方法。

然而,在这两种情况下,CPU 任务的调度方面是否有区别呢?

更新

请确认我的代码分析是否正确。

据我了解,await 会取消线程与正在运行该线程上的任务之间的关联。尽管这对于 I/O 请求非常有效,因为您现在可以重新使用已被网络驱动程序阻塞的 I/O 线程,但您需要维护 CPU 绑定的工作线程亲和性。

虽然 await 会破坏这种亲和性,但是 Wait() 方法,我不确定但仅仅是猜测,只是等待任务完成。如果任务还没有开始,它会在当前线程上执行它。然而,如果任务早已开始,它将阻塞当前线程,将当前线程放入等待队列中,直到下次执行调用 Wait 的任务的线程完成其工作并发出信号以唤醒等待线程。因此,调用 Wait 时会维护任务的线程亲和性。

当然,这些都只是猜测。我需要有人来确认它。


顺便提一下,async void 是用于事件处理程序的,而 LongIOBoundWorkWithSomeCPUBoundWorkAsWellAsync 看起来不像是一个事件处理程序。因此,很可能代码片段展示了错误使用 async void 的情况。 - undefined
4个回答

10
我理解await会取消线程与正在运行任务之间的任何关联,但实际情况并非如此。
首先,await不会导致任何东西运行或被“调度”。调度(如果有)和运行(如果有)在到达await之前已经在进行中了。 await是一种“异步等待”,也就是说,它会异步等待任务完成。这里的“异步”意味着“不阻塞当前线程”。
关于asyncawait的介绍,我在我的博客上有更详细的内容
我理解逻辑上两者的控制流相同,但实际上并非如此。 Wait会阻塞当前线程,而await则不会。
在这两种情况下,只有当CPUBoundWork任务完成执行后,才会调用DoIOAsync方法。
这是正确的。
但是,在这两种情况下,CPU任务的调度是否会有所不同呢?
不,无论哪种情况,都是由Task.Run而不是Wait或await进行调度。
虽然这对于I/O请求非常有效,因为现在可以重用被网络驱动程序阻塞的I/O线程,但您需要保持CPU绑定工作的线程亲和性。
如果要保持在同一线程上,则不能使用Task.Run。您必须直接调用CPUBoundWork。
关于Wait()方法,我不确定,但仅是猜测,它只是等待任务完成。如果任务尚未开始,则在当前线程上执行它。
事实上比这更复杂。有时候会发生,有时候不会。在您发布的代码中,通常不会(因为CPUBoundWork已经启动)。
因此,在调用Wait时,任务的线程亲和性得以保持。
再次强调,这只是一个简化。如果你所说的“线程亲和性”是指在Wait之前和之后使用同一线程,那么是的,没错。
然而,在异步方法中阻塞任务可能很危险;正如我在我的博客中所描述的那样,你可能会遇到死锁情况。如果你正在考虑将Task.Runasync一起使用,请查看我的Task.Run礼仪指南

非常感谢您的回复。我会更加努力学习,并回来重新阅读您的答案。 - Water Cooler v2
1
你有点忽略了问题陈述中的“现在重新使用了一个被网络驱动程序阻塞的I/O线程”...可能需要回顾一下"没有线程"(针对正在进行的重叠I/O)。 - Ben Voigt
或许你所说的是不同的,但在我这个不知情的人看来,现在看起来像是我的话用不同的词汇表达了你所说的内容。无论如何,在这个问题上我需要更多的学习之后再回来。 - Water Cooler v2
@StephenCleary 如果我在我的代码中放置了 await MyTask(),并且紧接着的下一行是 await MyTask2(),那么我的第二个任务(MyTask2())只有在MyTask()完成后才会被调用,对吗?唯一的问题是,由于使用了await而不是wait,因此在同一个线程中运行的所有进程都不会受到影响(暂停),因为我没有阻塞当前线程,对吗? - bmvr
是的。有关更多信息,请参阅我的异步介绍 - Stephen Cleary
显示剩余3条评论

3
一个简单的方法来理解async..await结构正在发生什么,就是将其“转换”为Task和ContinueWith。
我是什么意思?你的第一段代码:
async void LongIOBoundWorkWithSomeCPUBoundWorkAsWellAsync()
{
    await Task.Run(CPUBoundWork);

    // Do IO bound work
    await DoIOAsync();
}

将被转换为:

Task LongIOBoundWorkWithSomeCPUBoundWorkAsWellAsync()
{
    return Task.Run(CPUBoundWork)
        .ContinueWith(t => {
            // Do IO bound work
            DoIOAsync();
        }).Unwrap();
}

因此,它会立即返回一个任务链到调用方。

你的第二段代码:

async void LongIOBoundWorkWithSomeCPUBoundWorkAsWellAsync()
{
    var cpuTask = Task.Run(CPUBoundWork);

    cpuTask.Wait();

    // Do IO bound work
    await DoIOAsync();
}

将被转换为:

Task LongIOBoundWorkWithSomeCPUBoundWorkAsWellAsync()
{
    var cpuTask = Task.Run(CPUBoundWork);

    cpuTask.Wait();

    // Do IO bound work
    return DoIOAsync();
}

因此,它会等待第一个任务准备就绪,然后返回带有DoIOAsync任务的调用者。

因此,这两种方法明显不同!第二个代码将阻塞调用者,直到CPUBoundWork准备就绪。


谢谢。问题在于:我知道所有这些,并且已经在问题的更新部分中完全写下了所有这些。当我说逻辑控制流时,我并不是指在内部会发生什么。相反,我指的是按何种顺序执行哪个方法。而你们所有人正在解释的就是我所说的机械差异。每个人,包括Stephen Clearey在他的答案中所说的都是我在上面提问的更新部分中写的,尽管用的不同的词语。这是一个词语的区别,而不是意见的分歧。 - Water Cooler v2
@lvoros,在第一个例子中的.ContinueWith(...)后需要加上.Unwrap()才能达到相同的效果。此外,await做的不仅仅是这个:它检查任务是否已经完成,并且如果已完成,则继续执行。因此,在本质上,async方法可以在任何await或最终在返回或抛出时返回已经完成的任务。 - acelent
是的,你说得对。我已经纠正了它。谢谢你的解释。 - lvoros

1

就调度而言,一旦到达DoIOAsync,两种情况之间实际上没有区别,除了第一种情况可能在不同的线程上。

这里的主要区别是Wait()会阻塞线程,而await允许该线程被重用。阻塞线程比切换到新任务要昂贵得多。您还可能会给线程池增加更多压力。

当然,所有这些都要看你是否到达DoIOAsync。混合使用异步和同步是死锁的罪魁祸首,并且通常不建议使用,除非您绝对确定正在发生什么。


我已经更新了问题并提出了一个理论。如果有人能够确认或否定我的假设,那就太好了。 - Water Cooler v2
非常感谢您的回复。我会更加努力学习,并再次阅读您的答案。 - Water Cooler v2

0

我认为你在经过两个答案后已经明白了,在你的情况下不同之处在于Wait()会阻塞第二种变体。但是-在你的情况下,你没有从等待函数CPUBoundWork中返回任何值!但是如果你将返回一些值(例如int)-那么你的代码将像这样:

    Task<int> cpuTask = Task.Run(() => CPUBoundWork);
    ... // here we can do anything before CPUBoundWork return result
    var r = await cpuTask;// we will be here only after CPUBoundWork return result

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