它使编写异步代码大大变得更加容易。正如您在自己的问题中所指出的,它看起来就像您正在编写同步变体 - 但它实际上是异步的。
要理解这一点,您需要真正了解异步和同步的含义。意思其实很简单——同步的意思是一个接一个,一个接一个。异步意味着乱序。但这不是这里的全部情况——这两个词本身几乎没有用,它们的大部分含义来自上下文。你需要问:关于什么同步,究竟是什么?
假设您有一个需要读取文件的 Winforms 应用程序。在按钮单击中,您执行File.ReadAllText,并将结果放入某个文本框中 - 一切都很好。 I/O 操作相对于您的 UI 是同步的 - 在您等待 I/O 操作完成时,UI 不能做任何事情。现在,客户开始抱怨 UI 在读取文件时似乎会挂起几秒钟 - 并且 Windows 将应用程序标记为“无响应”。因此,您决定将文件读取委托给后台工作人员——例如,使用BackgroundWorker 或Thread。现在您的 I/O 操作相对于您的 UI 是异步的,每个人都很高兴 - 您所要做的就是提取您的工作并在自己的线程中运行它,耶。
现在,这实际上非常好 - 只要您一次只真正执行一个这样的异步操作。但是,这确实意味着您必须明确定义 UI 线程边界的位置——您需要处理正确的同步。当然,这在 Winforms 中非常简单,因为您可以使用 Invoke 将 UI 工作编组回 UI 线程 - 但是如果您需要在执行后台工作时反复与 UI 交互怎么办?当然,如果您只想连续发布结果,您可以使用 BackgroundWorkers ReportProgress - 但如果您还想处理用户输入怎么办?
await 的美妙之处在于,当您在后台线程上以及在同步上下文(例如 Windows 窗体 UI 线程)上时,您可以轻松管理:
string line;
while ((line = await streamReader.ReadLineAsync()) != null)
{
if (line.StartsWith("ERROR:")) tbxLog.AppendLine(line);
if (line.StartsWith("CRITICAL:"))
{
if (MessageBox.Show(line + "\r\n" + "Do you want to continue?",
"Critical error", MessageBoxButtons.YesNo) == DialogResult.No)
{
return;
}
}
await httpClient.PostAsync(...);
}
这太棒了——您基本上像往常一样编写同步代码,但相对于 UI 线程它仍然是异步的。并且错误处理再次与任何同步代码完全相同 - using、try-finally 和朋友们都工作得很好。
好的,所以你不需要到处撒BeginInvoke,有什么大不了的?真正重要的是,无需您付出任何努力,您实际上就开始为所有这些 I/O 操作使用真正的异步 API。问题是,就操作系统而言,实际上并没有任何同步 I/O 操作 - 当您执行“同步”File.ReadAllText 时,操作系统只是发布一个异步 I/O 请求,然后阻塞您的线程直到回复回来。应该很明显,线程在此期间什么也不做 - 它仍然使用系统资源,它为调度程序增加了少量工作等。
同样,在典型的客户端应用程序中,这没什么大不了的。用户不在乎你是有一个线程还是两个线程 - 差异并没有那么大。但是,服务器完全是另一种野兽。如果一个典型的客户端同时只有一个或两个 I/O 操作,您希望您的服务器能够处理数千个!在典型的 32 位系统上,您的进程中只能容纳大约 2000 个具有默认堆栈大小的线程 - 不是因为物理内存要求,而是因为耗尽了虚拟地址空间。 64 位进程不受限制,但启动新线程并销毁它们仍然相当昂贵,而且您现在正在向操作系统线程调度程序添加大量工作 - 只是为了让这些线程等待。
但是基于await 的代码没有这个问题。它仅在执行 CPU 工作时占用一个线程 - 等待 I/O 操作完成是不是 CPU 工作。因此,您发出该异步 I/O 请求,然后您的线程返回线程池。当响应到来时,从线程池中取出另一个线程。突然之间,您的服务器不再使用数千个线程,而是只使用了几个(通常每个 CPU 内核大约两个)。内存要求更低,多线程开销显着降低,总吞吐量增加不少。
所以 - 在客户端应用程序中,await 只是为了方便。在任何较大的服务器应用程序中,它都是必需 - 因为突然之间,您的“启动新线程”方法根本无法扩展。而使用 await 的替代方案是所有那些老式异步 API,它们处理没有像同步代码一样,并且处理错误非常乏味和棘手。