【问题标题】:What would be the difference between using the await keyword vs. the Task.Wait() method on a CPU bound task?在 CPU 绑定任务上使用 await 关键字与 Task.Wait() 方法有什么区别?
【发布时间】:2016-06-06 15:40:42
【问题描述】:

两者之间的机械有什么区别?

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();
}

我理解从逻辑上讲,两者都会产生相同的控制流。在这两种情况下,DoIOAsync 方法只会在CPUBoundWork 任务完成执行后被调用。

但是,这两种情况下CPU任务的调度会不会有区别?

更新

请确认我对上面代码的未受教育的思考是否正确。

据我了解,await 取消了线程与在线程上运行的任务之间的任何关联。虽然这对 I/O 请求非常有效,但由于您现在重用了在网络驱动程序上阻塞的 I/O 线程,您希望为 CPU 密集型工作保持线程关联性。 p>

虽然 await 破坏了这种亲和力,但 Wait() 方法,我不确定,但纯粹是猜测,只是等待任务完成。如果任务尚未开始,则在当前线程上执行。但是,如果任务已经开始较早,它会阻塞当前线程,将当前线程放在等待队列中,下一次执行调用Wait 的任务的线程回来并完成它的工作,它向等待线程发出信号,等待线程继续。因此,在调用Wait 时会保持任务的线程亲和性。

当然,所有这些都只是猜测。我想请人确认一下。

【问题讨论】:

    标签: c# .net asynchronous async-await task-parallel-library


    【解决方案1】:

    据我了解,await 会解除线程与在线程上运行的任务之间的任何关联。

    不,这根本不是发生的事情。

    首先,await 不会导致任何内容运行,也不会导致任何“计划”。在您到达await 之前,计划(如果有)和运行(如果有)已经在进行中。

    await 是“异步等待”;也就是说,它异步等待任务完成。这里的“异步”是指“不阻塞当前线程”。

    我的博客上有更详细的introduction to async and await

    我理解从逻辑上讲,两者都会产生相同的控制流。

    不是真的。 Wait 会阻塞当前线程,而await 不会。

    在这两种情况下,只有在 CPUBoundWork 任务完成执行后才会调用 DoIOAsync 方法。

    这是正确的。

    但是,这两种情况下CPU任务的调度会不会有区别?

    不,在这两种情况下,调度都是由 Task.Run 完成的,而不是 Waitawait

    虽然这对 I/O 请求非常有效,但由于您现在重用了在网络驱动程序上阻塞的 I/O 线程,因此您希望为 CPU 密集型工作保持线程关联性。

    如果你想留在同一个线程上,那么你不能使用Task.Run。您只需直接致电CPUBoundWork

    Wait() 方法,我不确定但纯粹是猜测,只是等待任务完成。如果任务尚未开始,则在当前线程上执行。

    除此之外还有更多内容。有时会,有时不会。在您发布的代码中,它通常不会(因为CPUBoundWork 已经开始了)。

    因此,在调用 Wait 时会保持任务的线程亲和性。

    同样,这是一个简化。如果“线程亲和性”是指Wait 前后的线程相同,那么是的,没错。

    但是,blocking on tasks within an asynchronous method can be dangerous;正如我在博客中所描述的那样,您可能会陷入僵局。如果您正在考虑将Task.Runasync 结合使用,请查看我的Task.Run etiquette guide

    【讨论】:

    • 非常感谢您的回复。我会研究更多,然后回来重新阅读您的答案。
    • 您有点掩饰了“您现在重新使用在网络驱动程序上被阻塞的 I/O 线程”的问题陈述......可能很好回顾"There is no thread"(用于飞行中重叠 I/O)
    • @StephenCleary:实际上。我在更新部分写的正是你所说的。我认为这是使用不同词语的问题。我应该进一步澄清。在阅读您和其他人链接到的所有文档和文章后,我会回来为这个问题添加更多内容并询问我想问的问题。
    • 或者可能是您所说的不同,但此时,在我不知情的情况下,看起来我所说的就是您用不同的话所说的。无论如何,在对这个主题进行更多研究之后,我需要回到这个问题。
    • @StephenCleary:我读了你的哲学文章,标题是“没有线程!”我相信你说的是我所说的,但是用了非常不同的词,并且非常详细和清晰地说明了操作系统和设备驱动程序中发生的事情。我喜欢你的解释,发现那篇文章非常有用。非常感谢。
    【解决方案2】:

    如果您将 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();
    }
    

    所以它一直等到第一个Task准备好,然后带着DoIOAsync的Task返回给调用者。

    所以这两种方法肯定不一样!第二个代码将阻塞调用者,直到 CPUBoundWork 准备好。

    【讨论】:

    • 谢谢。事情是:我知道这一切,并在我的问题的 Update 部分中写了 exactly 所有这些。当我说逻辑流程控制时,我并不是指内部会发生什么。相反,我的意思是按照什么顺序执行什么方法。你们都在解释的是我所说的机械差异。包括 Stephen Clearey 在内的每个人在他的回答中所说的正是我在上述问题的 Update 部分中所写的,尽管措辞不同。这里的区别是用词,而不是意见。
    • @lvoros,您需要在第一个示例中的.ContinueWith(...) 之后使用.Unwrap() 才能获得相同的效果。此外,await 做的还不止这些:它检查任务是否已经完成,如果是,则继续执行。因此,从本质上讲,async 方法可以在任何await 处返回一个任务,或者最终在返回或抛出时返回一个已经完成的任务。
    • 是的,你是对的。我已经纠正了。谢谢你的解释。
    【解决方案3】:

    就调度而言,一旦到达DoIOAsync,上下文之间就不会真正有区别,除了第一个可能在不同的线程上。

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

    当然,这就是 如果你到达DoIOAsync。混合使用异步和同步会导致死锁,如果您不确定发生了什么,通常不建议这样做。

    【讨论】:

    • 我已经用我的理论更新了这个问题。如果有人能证实或推翻我的假设,那就太好了。
    • 非常感谢您的回复。我会研究更多,然后回来重新阅读您的答案。
    【解决方案4】:

    我认为你在 2 个答案后理解了 jet,你的情况的不同之处在于 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
    

    【讨论】:

      猜你喜欢
      • 2019-03-11
      • 2022-01-27
      • 2014-06-01
      • 2014-06-08
      • 1970-01-01
      • 1970-01-01
      • 2022-01-20
      • 2014-01-15
      • 2020-01-09
      相关资源
      最近更新 更多