我试图用最新版本将所有Async内容添加到.NET框架中。我理解其中的一些,但说实话,我个人认为它不会让编写异步代码更容易。我发现它大部分时间都比较混乱,实际上比我们在async / await出现之前使用的传统方法更难阅读。
无论如何,我的问题很简单。我看到很多像这样的代码:
var stream = await file.readAsStreamAsync()
这里发生了什么?这不等于只调用方法的阻塞变体,即
var stream = file.readAsStream()
如果是这样,那么在这里使用它的重点是什么?它不会使代码更容易阅读,所以请告诉我我错过了什么。
答案 0 :(得分:9)
两次通话的结果都是一样的。
不同之处在于var stream = file.readAsStream()
将阻止调用线程直到操作完成。
如果从UI线程在GUI应用程序中进行调用,则应用程序将冻结,直到IO完成。
如果在服务器应用程序中进行了调用,则被阻止的线程将无法处理其他传入请求。线程池必须创建一个新线程来“替换”被阻塞的线程,这是很昂贵的。可扩展性将受到影响。
另一方面,var stream = await file.readAsStreamAsync()
不会阻止任何线程。 GUI应用程序中的UI线程可以使应用程序响应,服务器应用程序中的工作线程可以处理其他请求。
当异步操作完成时,操作系统将通知线程池,并且将执行该方法的其余部分。
为了使所有这些'魔术'成为可能,async / await的方法将被编译到状态机中。 Async / await允许使复杂的异步代码看起来像同步代码一样简单。
答案 1 :(得分:3)
这使得编写异步代码非常容易。正如您在自己的问题中所指出的那样,它看起来像,就好像您正在编写同步变体一样 - 但它实际上是异步的。
要理解这一点,您需要真正了解异步和同步的含义。意思很简单 - 同步意味着一个接一个的序列。异步意味着不按顺序。但这不是全部的图片 - 这两个词本身几乎没用,大多数意义都来自语境。你需要问:与什么同步?
假设您有一个需要读取文件的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窗体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
,有什么大不了的?真正重要的是,您没有任何努力,实际上开始使用真正的异步API进行所有这些I / O操作。问题是,就操作系统而言,实际上没有任何同步I / O操作 - 当你执行“同步”File.ReadAllText
时,操作系统只是发布异步I / O请求,然后阻止你的线程,直到响应回来。显而易见,线程在此期间无所作为 - 它仍然使用系统资源,它为调度程序等添加了少量工作。
同样,在典型的客户端应用程序中,这不是什么大问题。用户不关心你是否有一个或两个线程 - 差异并不是那么大。但服务器完全是一个不同的野兽;典型客户端同时只有一个或两个I / O操作,您希望服务器处理数千个!在典型的32位系统上,您只能在流程中使用默认堆栈大小来容纳大约2000个线程 - 不是因为物理内存要求,而是因为耗尽了虚拟地址空间。 64位进程并不受限制,但是仍然需要启动新线程并销毁它们是相当昂贵的,现在你正在为OS线程调度程序添加大量工作 - 只是为了让这些线程保持等待。
但是基于await
的代码没有这个问题。它在CPU工作时只占用一个线程 - 等待I / O操作完成不 CPU工作。因此,您发出该异步I / O请求,并且您的线程返回到线程池。当响应到来时,另一个线程从线程池中获取。突然间,您的服务器只使用了一对(通常每个CPU核心大约两个),而不是使用数千个线程。内存需求较低,多线程开销显着降低,总吞吐量也相当大。
所以 - 在客户端应用程序中,await
只是一个方便的事情。在任何更大的服务器应用程序中,它都是必需 - 因为突然你的“开始新线程”方法根本无法扩展。而使用await
的替代方法就是那些老式的异步API,它们处理 nothing 就像同步代码一样,并且处理错误非常繁琐且棘手。
答案 2 :(得分:2)
这里发生了什么?这不等于只是调用了 阻止该方法的变种,即
不,这不是阻止通话。这是编译器用来创建状态机的语法糖,在运行时将用于异步执行代码。
它使您的代码更具可读性,几乎与同步运行的代码类似。
答案 3 :(得分:0)
var stream = await file.readAsStreamAsync();
DoStuff(stream);
在概念上更像是
file.readAsStreamAsync(stream => {
DoStuff(stream);
});
在完全读取流时自动调用lambda。您可以看到这与阻止代码完全不同。
如果您正在构建UI应用程序,并实现按钮处理程序:
private async void HandleClick(object sender, EventArgs e)
{
ShowProgressIndicator();
var response = await GetStuffFromTheWebAsync();
DoStuff(response);
HideProgressIndicator();
}
与类似的同步代码不同:
private void HandleClick(object sender, EventArgs e)
{
ShowProgressIndicator();
var response = GetStuffFromTheWeb();
DoStuff(response);
HideProgressIndicator();
}
因为在第二个代码中UI会锁定,你永远不会看到进度指示器(或者最好它会短暂闪烁),因为UI线程将被阻塞,直到完成整个点击处理程序。在第一个代码中,进度指示器显示,然后UI线程在后台进行Web调用时再次运行,然后当Web调用完成时,DoStuff(response); HideProgressIndicator();
代码在UI线程上进行调度,并且很好地完成它的工作并隐藏了进度指标。
答案 4 :(得分:0)
看起来你错过了所有这个async / await
概念。
关键字async
让编译器知道该方法可能需要执行一些异步操作,因此它不应该像任何其他方法那样以正常方式执行,而应该将其视为状态机。这表明编译器将首先只执行方法的一部分(让我们称之为第1部分),然后在释放调用线程的其他线程上启动一些异步操作。编译器还将安排第2部分在ThreadPool
的第一个可用线程上执行。如果异步操作没有用关键字await
标记,那么它就不会被等待,并且调用线程继续运行直到方法完成。在大多数情况下,这是不可取的。那时我们需要使用关键字await
。
典型情况是:
线程1进入异步方法并执行代码Part1 - >
线程1启动异步操作 - >
线程1被释放,正在进行操作Part2安排在TP - >
某些线程(很可能是相同的线程1是免费的)继续运行方法直到结束(第2部分) - >