async/await的使用场景是什么?

14 浏览
0 Comments

async/await的使用场景是什么?

C#提供了多种执行异步操作的方式,如线程、futures和async。\n在哪些情况下,async是最佳选择呢?\n我已经阅读了许多关于async的文章,但迄今为止我还没有看到任何一篇讨论其原因的文章。\n起初,我以为async是一种内置的机制来创建future。类似于\n

async int foo(){ return ..复杂操作..; }
var x = await foo();
do_something_else();
bar(x);

\n其中对\'await foo\'的调用将立即返回,而对\'x\'的使用将等待\'foo\'的返回值。然而,async并不是这样工作的。如果你想要这种行为,你可以使用futures库:https://msdn.microsoft.com/en-us/library/Ff963556.aspx\n上面的例子将变成类似于\n

int foo(){ return ..复杂操作..; }
var x = Task.Factory.StartNew(() => foo());
do_something_else();
bar(x.Result);

\n这并不像我希望的那样漂亮,但它仍然可以工作。\n因此,如果你有一个想要让多个线程操作工作的问题,那么可以使用futures或其中一个并行操作,如Parallel.For。\n因此,async/await可能并不适用于并行执行工作以增加吞吐量的用例。

0
0 Comments

async/await的使用场景是解决在应用程序中处理大量异步事件(例如I/O)时的问题,当创建许多线程会非常昂贵时。

想象一下一个Web服务器,请求一旦到达就立即进行处理。处理过程在一个单独的线程上进行,每个函数调用都是同步的。为了完全处理一个线程可能需要几秒钟的时间,这意味着直到处理完成之前整个线程都被占用。

服务器编程的一种天真的方法是为每个请求生成一个新线程。这种方式不管每个线程花费多长时间完成,因为没有任何线程会阻塞其他线程。这种方法的问题在于线程不便宜。底层操作系统在耗尽内存或其他资源之前只能创建很多线程。一个使用每个请求1个线程的Web服务器可能无法扩展到几百/几千个每秒的请求。c10k挑战要求现代服务器能够扩展到10,000个同时用户。[http://www.kegel.com/c10k.html](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_address`和`find_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);
     }
  }
}

这是继续传递风格,其中“下一个要执行的任务”作为第二个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(address)`。

鉴于我目前对async的理解是关于线程管理的,我认为async在UI编程中并没有太多意义。通常UI不会有太多需要同时处理的事件。在UI中使用async的用例是防止UI线程被阻塞。尽管可以在UI中使用async,但我认为这是危险的,因为如果由于意外或遗忘而在某个长时间运行的函数上忽略了`await`,则UI将被阻塞。

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

这段代码不会阻塞UI,因为它在调用长时间运行的函数时使用了`await`。如果稍后添加了另一个函数调用:

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/await或CPS)是类似Rx的流模型。与CPS/Rx等相比,使用(或“坚持使用”)async/await的好处在于它仍然允许线性/过程化的程序流程,就像使用futures一样。

从我现在的观点来看,使用`Thread(...).Start()`不是启动线程的推荐方法。如果需要非线程池线程,请使用`Task.Run`或使用`Task.Factory.StartNew(..., TaskCreationOptions.LongRunning, TaskScheduler.Default)`。

到目前为止,这是对异步处理在Web服务器中的含义和用例的最好的解释。谢谢!

解释了异步编程的含义和使用案例,但没有解释async/await的语言构造。

0