我试图围绕使用更新版本添加到.NET框架中的所有Async内容.我理解其中的一些,但说实话,我个人认为这不会使编写异步代码更容易.我觉得它在大多数时候比较混乱,实际上比我们在async/await出现之前使用的更传统的方法更难阅读.
无论如何,我的问题很简单.我看到很多像这样的代码:
var stream = await file.readAsStreamAsync()
这里发生了什么?这不等于只调用方法的阻塞变体,即
var stream = file.readAsStream()
如果是这样,在这里使用它有什么意义呢?它不会使代码更容易阅读,所以请告诉我我错过了什么.
两次通话的结果都是一样的.
区别在于var stream = file.readAsStream()
将阻止调用线程直到操作完成.
如果从UI线程在GUI应用程序中进行调用,则应用程序将冻结,直到IO完成.
如果在服务器应用程序中进行调用,则被阻止的线程将无法处理其他传入请求.线程池必须创建一个新线程来"替换"被阻塞的线程,这是很昂贵的.可扩展性将受到影响.
另一方面,var stream = await file.readAsStreamAsync()
不会阻止任何线程.GUI应用程序中的UI线程可以使应用程序响应,服务器应用程序中的工作线程可以处理其他请求.
当异步操作完成时,OS将通知线程池,并且将执行该方法的其余部分.
为了使所有这些"魔法"成为可能,使用async/await的方法将被编译到状态机中.Async/await允许使复杂的异步代码看起来像同步代码一样简单.
它使编写异步代码变得非常容易。正如您在自己的问题中指出的那样,似乎您正在编写同步变量-但它实际上是异步的。
要了解这一点,您需要真正了解异步和同步的含义。含义非常简单-序列中的同步意味着一个接一个。异步意味着乱序。但是,这还不是全部内容-这两个词本身几乎没有用,它们的大部分含义来自上下文。您需要问:关于什么同步,到底是什么?
假设您有一个需要读取文件的Winforms应用程序。在按钮单击中,您执行File.ReadAllText
,然后将结果放在一些文本框中-一切正常。I / O操作相对于您的UI是同步的-等待I / O操作完成时,UI无法执行任何操作。现在,客户开始抱怨在读取文件时UI似乎挂起了几秒钟-Windows将应用程序标记为“无响应”。因此,您决定将文件读取委派给后台工作人员-例如,使用BackgroundWorker
或Thread
。现在,您的I / O操作相对于UI是异步的,每个人都很高兴-您要做的就是提取工作并在自己的线程中运行它,是的。
现在,这实际上非常好-只要您一次只真正执行一次这样的异步操作即可。但是,这确实意味着您必须明确定义UI线程边界的位置-您需要处理适当的同步。当然,这在Winforms中非常简单,因为您可以Invoke
将UI工作编组回UI线程-但是如果您需要在进行后台工作时重复与UI交互该怎么办?当然,如果您只想连续发布结果,可以使用BackgroundWorker
s- ReportProgress
但是如果您还想处理用户输入怎么办?
这样做的好处await
是,您可以轻松地在后台线程上以及在同步上下文中(例如Windows Forms 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位进程并没有受到限制,但是仍然存在启动新线程并销毁它们的过程相当昂贵的问题,并且您现在正在为OS线程调度程序添加大量工作-只是为了使这些线程保持等待状态。
但是await
基于代码的没有这个问题。它仅在执行CPU工作时占用一个线程-等待I / O操作完成不是 CPU工作。因此,您发出该异步I / O请求,并且您的线程返回到线程池。当响应到来时,将从线程池中获取另一个线程。突然,您的服务器只使用了几个线程(通常每个CPU核心大约两个),而不是使用数千个线程。内存需求降低,多线程开销显着降低,您的总吞吐量大大提高。
因此-在客户端应用程序中,await
实际上仅仅是为了方便。在任何较大的服务器应用程序中,这都是必要的 -因为突然之间,“启动新线程”方法根本无法扩展。替代方法是使用await
所有老式的异步API,这些API 不能像同步代码那样处理任何东西,并且处理错误非常繁琐和棘手。