Brief explanation of Async/Await in .Net 4.5
(3个答案)
5年前关闭。
C#提供了多种执行异步执行的方式,例如线程, future 和异步。
在什么情况下异步是最佳选择?
我已经阅读了许多有关如何以及如何进行异步的文章,但是到目前为止,我还没有看到任何讨论其原因的文章。
最初,我认为异步是创建 future 的内置机制。就像是
async int foo(){ return ..complex operation..; }
var x = await foo();
do_something_else();
bar(x);
对“await foo”的调用将立即返回,而对“x”的使用将在“foo”的返回值上等待。异步不会这样做。如果您想要这种行为,则可以使用 future 库:
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);
虽然不如我所希望的那么漂亮,但是它仍然可以工作。
因此,如果您有一个问题需要在工作中使用多个线程,则可以使用Future或并行操作之一,例如Parallel.For。
那么,异步/等待可能并不适合并行执行工作以提高吞吐量的用例。
异步解决了在创建大量线程时需要为大量异步事件(例如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_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);
}
执行进程的线程在进入look_up_address时会脱离该函数,并继续执行其他工作:例如处理其他请求。当look_up_address完成并准备好继续进行处理时,某个线程(或同一线程)将从上一个线程中断的地方开始,并执行下一行find_tax_forms(address)。
由于我目前对异步的信念是管理线程,因此我认为异步对于UI编程没有多大意义。通常,UI不需要处理那么多同时发生的事件。与UI异步的用例是防止UI线程被阻塞。即使异步可以与UI一起使用,我也会发现它很危险,因为由于意外或健忘而忽略了一些长时间运行的功能,会导致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线程,并且创建太多线程的机会非常小。
我是一名优秀的程序员,十分优秀!