当前位置: 首页 > 科技观察

个人经验和实例全面解析C#异步编程

时间:2023-03-14 11:48:34 科技观察

我们在处理一些长时间的调用时,经常会导致界面停止响应或者IIS线程占用过多。这时候,我们就需要更多的使用异步编程来纠正这些问题,这些问题通常说起来容易做起来难。诚然,异步编程与同步编程相比,是一种完全不同的编程思想。对于习惯了同步编程的开发者来说,在开发过程中是有难度的。更大、更难控制是它的特点。在.NETFramework5.0中,微软为我们引入了新的语言特性,让我们可以像使用同步编程一样接近和简单地使用异步编程。本文将讲解Framework之前版本中基于回调的异步编程模型。一些限制和新的API使我们能够轻松地完成相同的开发任务。为什么要异步?长期以来,使用远程资源进行编程一直是一个令人困惑的问题。与“本地资源”不同,访问远程资源总是会出现很多意想不到的情况。网络环境不稳定,机器服务器故障。这就造成了很多程序员完全无法控制的问题,所以这也需要程序员保护远程资源调用,管理调用取消,超市,线程等待,处理长时间不响应的线程。在.NET中,我们通常会忽略这些挑战。事实上,我们有多种不同的模式来处理异步编程,比如在处理IO密集型操作或高延迟操作时不对线程进行分组。在大多数情况下,我们有同步和异步两种方法来做这件事。但问题是目前的这些模型很容易造成混乱和代码错误,或者开发者会放弃而采用阻塞的方式进行开发。在今天的.NET中,它提供了一种非常接近于同步编程的编程体验,并且不需要开发人员处理许多只有在异步编程中才会出现的情况。异步调用将是清晰不透明的,并且容易与同步代码结合。过去不好的体验理解这种问题最好的方法是我们最常见的情况:用户界面只有一个线程,所有的工作都运行在这个线程上,客户端程序无法响应用户的鼠标时间,这是最可能是因为应用程序被耗时的操作阻塞。这可能是因为线程正在等待网络ID或执行CPU密集型计算。这时候用户界面获取不到运行时间,程序一直处于忙碌状态,这是一种很差的用户体验。多年来,解决这个问题的方法都是异步调用,不等待响应,尽快返回请求,这样其他事件可以同时执行,只有在响应发生时才通知应用程序request有一个最终的反馈,以便客户端代码可以执行指定的代码。问题是:异步代码完全破坏了代码流,回调代理解释了之后如何工作,但是如何在while循环中等待?if语句?try块还是using块?如何解释“下一步该做什么”?请参见下面的示例:publicintSumPageSizes(IListuris){inttotal=0;foreach(varuriinuris){txtStatus.Text=string.Format("Found{0}bytes...",total);vardata=newWebClient().DownloadData(uri);total+=data.Length;}txtStatus.Text=string.Format("Found{0}bytestotal",total);returntotal;}此方法从uri列表下载文件并计算其大小并更新同时显示状态信息,显然这个方法不属于UI线程,因为需要很长时间才能完成,所以会把UI彻底挂掉,但是我们又想让UI不断更新,怎么办呢?我们可以创建一个后台程序,不断向UI线程发送数据,让UI自行更新。这看起来很浪费,因为这个线程大部分时间都在等待和下载,但有时,这正是我们需要做的。在这个例子中,WebClient提供了一个异步版本的DownloadData方法——DownloadDataAsync,它立即返回,然后在DownloadDataCompleted之后触发一个事件,这允许用户编写一个异步版本的方法,将需要做的事情拆分开来,并且call立即返回并完成对UI线程的后续调用,从而不再阻塞UI线程。这是第一次尝试:publicvoidSumpageSizesAsync(IListuris){SumPageSizesAsyncHelper(uris.GetEnumerator(),0);}publicvoidSumPageSizesAsyncHelper(IEnumeratorenumerator,inttotal){if(enumerator.MoveNext()){txtStatus。Text=string.Format("Found{0}bytes...",total);varclient=newWebClient();client.DownloadDataCompleted+=(sender,e)=>{SumPageSizesAsyncHelper(enumerator,total+e.Result.Length);};client.DownloadDataAsync(enumerator.Current);}else{txtStatus.Text=string.Format("Found{0}bytestotal",total);}}然后这仍然很糟糕,我们打破了一个整洁的foreach循环并得到手动枚举器,每次调用都会创建一个事件回调。该代码用递归替换了循环。你应该不敢直视这种代码。别担心,这还没有结束。在原始代码返回总数并显示它的地方,新的单步版本在计数完成之前返回给调用者。我们如何才能将结果返回给调用者?答案是:调用者必须支持回调,我们统计完成后才可以调用。但是异常呢?原始代码不关注异常,它一直传递给调用者,在异步版本中我们必须向后扩展让异常传播,我们必须在异常发生时明确让它传播。最终,这些需求会使代码更加混乱:inttotal,Actioncallback){try{if(enumerator.MoveNext()){txtStatus.Text=string.Format("Found{0}bytes...",total);varclient=newWebClient();client.DownloadDataCompleted+=(sender,e)=>{SumPageSizesAsyncHelper(enumerator,total+e.Result.Length,callback);};client.DownloadDataAsync(enumerator.Current);}else{txtStatus.Text=string.Format("Found{0}bytestotal",total);enumerator.Dispose();callback(total,null);}}catch(Exceptionex){enumerator.Dispose();callback(0,ex);}}当你看这些再次编码时,你能立刻分辨出这是什么JB东西吗?恐怕不是,我们只是想像同步方法一样用异步调用来代替阻塞调用,把它包裹在foreach循环中,考虑尝试组合更多的异步调用或者有更复杂的控制结构,这不是一个大小SubPageSizesAsync可以解决。我们真正的问题是我们无法再解释这些方法中的逻辑,我们的代码完全乱了。异步代码中的大量工作使得整个代码难以阅读并且似乎充满了错误。#p#Anewway现在,我们有了一个新的功能来解决上面的问题,异步版本的代码将如下:publicasyncTaskSumPageSizesAsync(IListuris){inttotal=0;foreach(varuriinuris){txtStatus.Text=string.Format("Found{0}bytes...",total);vardata=awaitnewWebClient().DownloadDataTaskAsync(uri);total+=data.Length;}txtStatus.Text=string.Format("Found{0}bytestotal",total);returntotal;}上面的代码除了增加了高亮部分外,和同步版的代码非常相似,代码的流程一直没变,我们也没有看到任何回调,但这并不意味着没有回调操作,编译器会处理这些工作,你不再需要关心它们。异步方法使用Task替换原来返回的Int类型。今天的框架中提供了Task和Task来表示正在运行的作业。异步方法没有额外的方法。按照惯例,为了区分同步版本的方法,我们在方法名后面加上Async作为新的方法名。上面的方法也是异步的,这意味着方法体会被编译器区别对待,允许其中一部分成为回调,并自动创建Task作为返回类型。关于该方法的说明:在该方法内部,调用了另一个异步方法DownloadDataTaskAsync,该方法快速返回一个Task类型的变量,在下载数据完成后激活。和以前一样,当数据不是完成之前我们不想做任何事情,所以我们使用await来等待操作完成。貌似await关键字会阻塞线程,直到任务下载的数据完成,其实不然。相反,它标记任务的回调并立即返回。当任务完成时,它将执行回调。TasksTask和Task类型已存在于.NETFramework4.0中。任务代表正在进行的活动。它可能是CPU密集型作业或在单独线程中运行的IO操作。手动创建一个不在单独线程中工作的任务也很容易:vartcs=newTaskCompletionSource();stream.BeginRead(buffer,0,buffer.Length,arr=>{varlength=stream.EndRead(arr);tcs.SetResult(stream.Length);},null);returncs。Task;}一旦创建了一个TaskCompletionSource对象,就可以返回与其关联的Task对象,相关工作完成后客户端代码将得到最终结果。此时Task不占用自己的线程。如果实际任务失败,Task可以携带异常向上传播。如果使用await,客户端代码会触发异常:}catch(Exceptionex){Console.WriteLine(ex.Message);}}staticTaskReadFileAsync(stringfilePath,outbyte[]buffer){Streamstream=File.Open(filePath,FileMode.Open);buffer=newbyte[stream.Length];vartcs=newTaskCompletionSource();stream.BeginRead(buffer,0,buffer.Length,arr=>{try{varlength=stream.EndRead(arr);tcs.SetResult(stream.Length);}catch(IOExceptionex){tcs.SetException(ex);}},null);returntcs.Task;}基于任务的异步编程模型上面解释了异步方法应该是什么样子——基于任务的异步模式(TAP),上面的异步实施例仅需要调用方法和返回Task或Task的异步异步方法。下面将介绍TAP中的一些约定,包括如何处理“取消”和“进行中”,我们将进一步说明基于任务的编程模型。async和await了解async方法不在自己的线程中运行非常重要,事实上,编写一个没有任何await的async方法将是一个完全同步的方法:staticasyncTaskTenToSevenAsync(){awaitTask.Delay(3000);return7;}如果你调用这个方法,它会阻塞线程10秒并返回7,这可能不是你期望的,你也会在VS中得到一个警告,因为这可能永远不是你想要的desired结果。async方法只有在遇到await语句时才会立即将控制权返回给调用者,但是直到等待的任务完成后才真正返回结果,这意味着你需要确保async方法中的代码不会执行太多任务或阻止性能调用。下面的例子是你期望的效果staticasyncTaskTenToSevenAsync(){awaitTask.Delay(3000);return7;}Task.Delay其实是异步版本的Tread,Sleep,它返回一个Task,这个Task会在分配的时间。不返回任何值的事件处理程序和异步方法可以使用其他异步方法中的await创建异步方法,但是异步在哪里结束?在客户端程序中,通常的答案是异步方法由事件启动,用户单击按钮,异步方法被激活直到完成。方法执行何时完成,事件本身并不重要。这通常被称为“即发即弃”为了适应这种模式,异步方法通常被显式设计为“即发即弃”——使用void作为返回值而不是Task类型,这允许方法直接作为事件处理程序。执行voidsaync方法时,不返回任何Task,调用者无法跟踪调用是否完成。privateasyncvoidsomeButton_Click(objectsender,RoutedEventArgse){someButton.IsEnabled=false;awaitSumPageSizesAsync(GetUrls()));someButton.IsEnabled=true;}你写的结论越多,你就越不人性化。....