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_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);
}
}
}
这是延续传递风格,其中 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线程被阻塞的可能,创建过多线程的可能性很小。