async/await的用例是什么?

jon*_*onr 1 .net c# multithreading asynchronous async-await

C#提供了多种执行异步执行的方法,如线程,期货和异步.

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

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

最初我认为async是一种创建未来的内置机制.就像是

async int foo(){ return ..complex operation..; }
var x = await foo();
do_something_else();
bar(x);
Run Code Online (Sandbox Code Playgroud)

对'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);
Run Code Online (Sandbox Code Playgroud)

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

因此,如果您有一个问题,您希望让多个线程对作品进行操作,那么请使用future或其中一个并行操作,例如Parallel.For.

因此,async/await可能不适用于并行执行工作以增加吞吐量的用例.

jon*_*onr 9

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);
}
Run Code Online (Sandbox Code Playgroud)

其中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);
     }
  }
}
Run Code Online (Sandbox Code Playgroud)

这是继续传递样式,其中要做的下一件事作为第二个lambda传递给一个函数,该函数在调用阻塞操作(在第一个lambda中)时不会阻塞当前线程.这有效,但它很快变得非常丑陋,很难遵循程序逻辑.

程序员在分割程序时手动完成的操作可以通过async/await自动完成.每次调用I/O函数时,程序都可以通过等待来标记该函数调用,以通知调用者程序它可以继续执行其他操作而不是等待.

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);
}
Run Code Online (Sandbox Code Playgroud)

执行进程的线程在进入look_up_address并继续执行其他工作时将突破该函数:例如处理其他请求.当look_up_address已经完成并且进程准备好继续时,一些线程(或相同的线程)将在最后一个线程停止的地方拾取并执行下一行find_tax_forms(地址).

由于我目前对异步的看法是关于管理线程,我不相信异步对UI编程很有意义.通常,UI不会有那么多需要处理的同时事件.与UI异步的用例是阻止UI线程被阻止.即使异步可以与UI一起使用,我也会发现它很危险,因为在一些长时间运行的函数中省略等待,由于意外或遗忘,会导致UI被阻塞.

async void button_callback(){
   await do_something_long();
   ....
}
Run Code Online (Sandbox Code Playgroud)

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

async void button_callback(){
   do_another_thing();
   await do_something_long();
   ...
}
Run Code Online (Sandbox Code Playgroud)

如果程序员添加了对do_another_thing的调用只是需要执行多长时间,那么现在将阻止UI.总是在后台线程中执行所有处理似乎更安全.

void button_callback(){
  new Thread(){
    do_another_thing();
    do_something_long();
    ....
  }.start();
}
Run Code Online (Sandbox Code Playgroud)

现在不可能阻止UI线程,并且创建太多线程的可能性非常小.