【问题标题】:Multiple await operations or just one多个等待操作或只有一个
【发布时间】:2016-08-09 02:26:30
【问题描述】:

我已经看到了await 关键字是如何实现的以及它创建的结果结构。我想我对它有一个初步的了解。然而,是

public async Task DoWork()
{
    await this.Operation1Async();
    await this.Operation2Async();
    await this.Operation3Async();
}

“更好”(一般来说)或

public async Task DoWork()
{
    await this.Operation1Async();
    this.Operation2();
    this.Operation3();
}

第一种方法的问题是它为每个等待调用创建一个新的Task?哪个需要一个新线程?

而第一个在第一个await 上创建一个新的Task,然后从那里开始的所有内容都在新的Task 中处理?

编辑 好吧,也许我不太清楚,但如果我们有

while (await reader.ReadAsync())
{
    //...
}

await reader.NextResultAsync();

// ...

这不是创建两个任务吗?一个在主线程中使用第一个ReadAsync,然后在这个新创建的任务中使用NextResultAsync 进行另一个任务。我的问题是真的需要第二个任务,不是吗 在主线程中创建的一项任务就足够了吗?所以

while (await reader.ReadAsync())
{
    //...
}

reader.NextResult();

// ...

【问题讨论】:

标签: c# asp.net-mvc asynchronous async-await


【解决方案1】:

它为每个等待调用创建一个新任务?哪个需要一个新线程?

是和不是。是的,它正在为每个异步方法创建一个Taskasync 状态机将创建一个。但是,这些任务不是线程,它们甚至也不在线程上运行。它们不会在任何地方“奔跑”。

您可能会发现我的一些博客文章很有用:

  • async intro,它解释了 async/await 的工作原理。
  • There Is No Thread,它解释了任务如何在没有线程的情况下工作。
  • Intro to the Task type,它解释了一些任务(Delegate Tasks)如何拥有代码并在线程上运行,但async(Promise Tasks)使用的任务却没有。

而第一个在第一个等待时创建一个新任务,然后从那里开始的所有内容都在新任务中处理?

一点也不。任务只完成一次,并且该方法不会继续超过await,直到该任务完成。所以,Operation1Async 返回的任务在Operation2 被调用之前就已经完成了。

【讨论】:

  • 这些都是很棒的博文!谢谢。
【解决方案2】:

这 2 个示例在功能上并不相同,因此您可以根据自己的具体需要选择一个。在第一个示例中,3 个任务按顺序执行,而在第二个示例中,第二个和第三个任务并行运行,无需等待其结果完成。同样在第二个示例中,DoWork 方法可能会在第二个和第三个任务完成之前返回。

如果您想确保在离开DoWork 方法体之前任务已经完成,您可能需要这样做:

public async Task DoWork()
{
    await this.Operation1Async();
    this.Operation2().GetAwaiter().GetResult();
    this.Operation3().GetAwaiter().GetResult();
}

这当然是非常糟糕的,你永远不应该这样做,因为它会阻塞主线程,在这种情况下你可以使用第一个示例。如果这些任务使用 I/O 完成端口,那么您绝对应该利用它们而不是阻塞主线程。

另一方面,如果您问是否应该使 Operation2Operation3 异步,那么答案是:如果他们正在做 I/O 绑定的东西,您可以利用 I/O 完成端口那么你绝对应该让它们异步并采用第一种方法。如果它们是您无法使用 IOCP 的 CPU 绑定操作,那么最好让它们保持同步,因为在一个单独的任务中执行此 CPU 绑定操作没有任何意义,无论如何您都会阻塞。

【讨论】:

  • 第二个例子中的 Operation2 和 Operation3 可能是非异步方法。
  • @EugenePodskal,根据他的第一个示例,它们非常异步,看看他是如何等待它们的。
  • 这可能是我们应该问 OP 的问题。但是 Operation2Async 和 Operation2 似乎是两种截然不同的方法。或者它可能只是一个错字。
  • 嗯,你是对的。这可能确实是一种不同的方法。在这种情况下,这个问题没有任何意义,因为这两个是完全不同的东西。我也会更新我的答案以涵盖这种可能性。感谢您指出这一点。
  • 我看到了您修改后的问题。我再次重复我多次说过的话。如果操作是 I/O 绑定的,那么您应该使用第一种方法和多个等待。即使这使用了多个任务,它们也被委派给操作系统中的底层 I/O 完成端口,因此它们的寿命非常短。与编写阻塞调用的第二种方法相比,这种方法效率更高。
【解决方案3】:

第一种方法的问题是它为每个等待调用创建一个新任务?哪个需要一个新线程?

这是您的误解,导致您对第一个示例中的代码产生怀疑。

Task 不需要新线程。如果您愿意,Task 当然可以在新线程上运行,但是任务的一个重要用途是当任务直接或间接通过异步 i/o 工作时,这发生在任务或它所在的任务中打开等待,使用异步 i/o 访问文件或网络流(例如 Web 或数据库访问),允许池中的线程返回到池中,直到 i/o 完成。

因此,如果任务没有立即完成(例如,如果它的目的可以完全从当前填充的缓冲区中完成,则可能发生这种情况),当前运行的线程可以返回到池中,并且可以用于在与此同时。当 i/o 完成时,池中的另一个线程可以接管并完成该等待,然后该线程可以完成等待该任务的任务中的等待,依此类推。

因此,您问题中的第一个示例允许总共使用更少的线程,尤其是当其他工作也将使用同一池中的线程时。

在第二个示例中,一旦第一个 await 完成,处理其完成的线程将阻塞同步等效方法。如果其他操作也需要使用池中的线程,那么该线程不会返回给它,则必须启动新线程。因此,第二个示例是需要更多线程的示例。

【讨论】:

  • 好的。让我们忽略线程,因为它的启动是优化的 :) 所以在我的数据读取器示例中,当异步 while 循环完成时,任务完成了吗?还是继续?然后当NextResultAsync 被击中时会创建一个新任务?而且ReadAsync 还会继续推出新任务吗?
  • “启动一个新任务”听起来类似于启动一个新线程。如果您循环通过ReadAsync 一百次,那么它将是一百个任务,然后是下一个任务的另一个任务。这虽然是一个线程可能被用于其他工作的一百次,而它本来可以被阻塞。尽管可以避免分配任务是值得的(例如,在适用时使用Task.CompletedTask()),就像我们通常通过避免分配进行优化一样,任务通常比阻塞线程便宜,因此您节省了 101 倍。
  • (值得注意的是,通过重用任务对象来避免分配任务的工作通常是在框架内完成的,所以有时你已经拥有了这个优势。还可以看看目前在 CoreFX 中的ValueTask用于更频繁地避免任务分配的等待对象)。
【解决方案4】:

一个并不比另一个更好,他们做的事情不同。

在第一个示例中,每个操作都在一个线程上调度和执行,由Task 表示。注意:不能保证它们发生在哪个线程上。

await 关键字意味着(松散地)“等到这个异步操作完成然后继续”。延续,也不一定在同一个线程上完成。

这个意思是例子一,是异步操作的同步处理。现在仅仅因为创建了Task,并不能推断出Thread 也被创建,TaskScheduler 使用的线程池已经创建,实际上引入的开销非常小。

在您的第二个示例中,await 将使用调度程序调用第一个操作,然后照常调用接下来的两个操作。没有为后两个调用创建TaskThread,也没有调用Task 上的方法。

在第一个示例中,您还可以考虑同时进行异步调用。这将安排所有三个操作“同时”运行(不保证),并等待它们全部执行完毕。

public async Task DoWork()
{
    var o1 = this.Operation1Async();
    var o2 = this.Operation2Async();
    var o3 = this.Operation3Async();

    await Task.WhenAll(o1, o2, o3);
}

【讨论】:

  • 请看我的修改!
  • @Umair 您误解了Task 的工作原理。这是一个工作单元。任务不会创建一次,然后重新用于顺序方法调用。通常,Task 用于短期 CPU 绑定工作,或使用 IO 完成端口的工作
猜你喜欢
  • 2018-04-03
  • 1970-01-01
  • 2012-11-25
  • 1970-01-01
  • 1970-01-01
  • 2021-08-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多