【问题标题】:What is the use case for async/await? [duplicate]异步/等待的用例是什么? [复制]
【发布时间】:2015-09-02 01:51:48
【问题描述】:

C# 提供了多种方式来执行异步执行,例如线程、future 和 async。

在什么情况下异步是最佳选择?

我已经阅读了很多关于异步的如何什么的文章,但到目前为止我还没有看到任何讨论为什么的文章.

最初我认为异步是一种创建未来的内置机制。类似的东西

async int foo(){ return ..complex operation..; }
var x = await foo();
do_something_else();
bar(x);

对“await foo”的调用将立即返回,而使用“x”将等待“foo”的返回值。 async 不这样做。如果你想要这种行为,你可以使用期货库:https://msdn.microsoft.com/en-us/library/Ff963556.aspx

上面的例子应该是这样的

int foo(){ return ..complex operation..; }
var x = Task.Factory.StartNew<int>(() => foo());
do_something_else();
bar(x.Result);

这并不像我希望的那样漂亮,但它仍然有效。

因此,如果您遇到希望让多个线程对工作进行操作的问题,请使用期货或并行操作之一,例如 Parallel.For。

因此,async/await 可能不适用于并行执行工作以提高吞吐量的用例。

【问题讨论】:

标签: c# .net multithreading asynchronous async-await


【解决方案1】:

async 解决了在创建大量线程代价高昂时为大量异步事件(例如 I/O)扩展应用程序的问题。

想象一个 Web 服务器,其中的请求在进入时立即得到处理。处理发生在单个线程上,其中每个函数调用都是同步的。完全处理一个线程可能需要几秒钟,这意味着在处理完成之前会消耗整个线程。

一种简单的服务器编程方法是为每个请求生成一个新线程。这样,每个线程需要多长时间才能完成并不重要,因为没有线程会阻塞任何其他线程。这种方法的问题是线程并不便宜。底层操作系统在内存或其他资源耗尽之前只能创建这么多线程。每个请求使用 1 个线程的 Web 服务器可能无法扩展到每秒数百/数千个请求。 c10k 挑战要求现代服务器能够扩展到 10,000 个并发用户。 http://www.kegel.com/c10k.html

更好的方法是使用线程池,其中存在的线程数或多或少是固定的(或者至少不会超过某个可容忍的最大值)。在这种情况下,只有固定数量的线程可用于处理传入请求。如果请求多于可用于处理的线程数,则某些请求必须等待。如果一个线程正在处理一个请求并且必须等待一个长时间运行的 I/O 进程,那么实际上该线程并没有被充分利用,并且服务器吞吐量将大大低于其他情况。

现在的问题是,我们如何才能拥有固定数量的线程但仍然有效地使用它们?一个答案是“切断”程序逻辑,这样当一个线程通常会等待一个 I/O 进程时,它会启动 I/O 进程,但会立即空闲给任何其他想要执行的任务。在 I/O 之后将要执行的程序部分将存储在一个知道如何在以后继续执行的事物中。

例如,原始同步代码可能看起来像

void process(){
   string name = get_user_name();
   string address = look_up_address(name);
   string tax_forms = find_tax_form(address);
   render_tax_form(name, address, tax_forms);
}

look_up_addressfind_tax_form 必须与数据库通信和/或向其他网站发出请求。

异步版本可能看起来像

void process(){
  string name = get_user_name();
  invoke_after(() => look_up_address(name), (address) => {
     invoke_after(() => find_tax_form(address), (tax_forms) => {
        render_tax_form(name, address, tax_forms);
     }
  }
}

这是延续传递风格,其中 next thing to do 作为第二个 lambda 被传递给在调用阻塞操作(在第一个 lambda 中)时不会阻塞当前线程的函数。这很有效,但很快就会变得非常丑陋且难以遵循程序逻辑。

程序员在拆分程序时手动完成的工作可以通过 async/await 自动完成。任何时候调用 I/O 函数,程序都可以用 await 标记该函数调用,以通知程序的调用者它可以继续做其他事情,而不仅仅是等待。

async void process(){
  string name = get_user_name();
  string address = await look_up_address(name);
  string tax_forms = await find_tax_form(address);
  render_tax_form(name, address, tax_forms);
}

执行process的线程会在到达look_up_address时跳出函数,继续做其他工作:比如处理其他请求。当 look_up_address 已经完成并且 process 准备好继续时,一些线程(或同一个线程)将在最后一个线程停止的地方接起并执行下一行 find_tax_forms(地址).

由于我目前对异步的看法是关于管理线程,因此我认为异步对 UI 编程没有多大意义。通常,UI 不会有那么多需要处理的同时发生的事件。与 UI 异步的用例是防止 UI 线程被阻塞。尽管 async 可以与 UI 一起使用,但我会发现它很危险,因为在某些长时间运行的函数上省略 await,由于意外或健忘,会导致 UI 阻塞。

async void button_callback(){
   await do_something_long();
   ....
}

此代码不会阻塞 UI,因为它对它调用的长时间运行的函数使用等待。如果稍后添加另一个函数调用

async void button_callback(){
   do_another_thing();
   await do_something_long();
   ...
}

将调用添加到 do_another_thing 的程序员不清楚执行需要多长时间,现在 UI 将被阻止。总是在后台线程中执行所有处理似乎更安全。

void button_callback(){
  new Thread(){
    do_another_thing();
    do_something_long();
    ....
  }.start();
}

现在不存在UI线程被阻塞的可能,创建过多线程的可能性很小。

【讨论】:

  • 处理异步代码(来自 async/awit 或 CPS)的另一种替代方法包括像 Rx 这样的流模型。与 CPS/Rx 之类的东西相比,使用(或“坚持”)异步/等待的好处是它仍然允许像期货一样的线性/过程程序流。
  • new Thread(...).Start() 不是推荐的启动线程的方式,恕我直言。如果需要非线程池线程,请使用Task.RunTask.Factory.StartNew(..., TaskCreationOptions.LongRunning, TaskScheduler.Default)
  • 到目前为止,最佳答案解释了 Web 服务器中异步处理的含义和用例。谢谢!!
  • Nive 的解释,但它实际上只解释了 aysnc 编程,而不是 async/await 的语言结构。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多