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

看了那么多爆款文章,还是走不上异步编程这条路?

时间:2023-03-23 01:43:06 科技观察

本文转载自微信公众号《精益码农》,作者为精益码农。转载本文请联系精益码农公众号。C#异步编程语法糖async/await使开发人员可以轻松编写异步代码。在这里和那里看了很多文章,很多都是临时抱佛脚(有些翻译还是有偏差)。遵守上面的冷②③原则,一般可以保证异步程序按预期运行。我们经常在各大论坛看到同学们(由于不遵守②③点)造成的僵局。async/await死锁导致的LiveUI程序(WinForm、WPF):点击按钮,触发HTTP请求,用请求结果修改UI控件,以下代码会导致死锁publicstaticasyncTaskGetJsonAsync(Uriuri){using(varclient=newHttpClient()){varjsonString=awaitclient.GetStringAsync(uri);returnjsonString;}}//上层调用方法publicvoidButton1_Click(...){varjsonTask=GetJsonAsync(...);textBox1.Text=jsonTask.Result;}ASP.NETWeb程序:从api接口发起HTTP请求,返回请求结果。下面的代码也会触发死锁publicstaticasyncTaskGetJsonAsync(Uriuri){using(varclient=newHttpClient()){varjsonString=awaitclient.GetStringAsync(uri);returnjsonString;}}//上层调用方法publicclassMyController:ApiController{publicstringGet(){varjsonTask=GetJsonAsync(...);returnjsonTask.Result;}}有两种编程方式可以解决上面的死锁:不再混合异步/同步写法,总是使用async/await语法糖写异步代码并将ConfigureAwait(false)方法应用到等待的异步任务中,解决死锁大有裨益。本文将讲解:async/await的工作机制异步编程语法糖中SynchronizationContext的意义示例代码为什么死锁1.第二步:在调用异步方法GetStringAsync时,启动异步任务;第六步:当遇到await关键字时,框架会捕获调用线程的同步上下文(SynchronizationContext)对象,附加到异步任务中;同时将控制权交给上层调用Function;第七步:异步任务完成,通过IO完成端口通知上层线程。第八步:通过捕获到的线程同步上下文执行后续代码块;2、SynchronizationContext的含义首先看MSDN中SynchronizationContext的定义:ProvidedinBasicfunctionalityforpropagatingsynchronizationcontextsinvarioussynchronizationmodels。此类实现的同步模型的目的是允许公共语言运行时的内部异步/同步操作使用不同的同步模型正常运行。??这不是人们能够理解的解释。我给出的解释是:线程切换过程中保存了调用线程的上下文,用于异步任务完成后使用这个线程同步上下文执行后续代码。线程同步上下文是什么意思?大家都知道:WinForm和WPF有着相似的原理:在后台执行耗时较长的任务,将结果异步返回给UI线程。(这就是为什么ConfigureAwait方法默认传递true的原因?)此时,需要捕获UI线程的SynchronizationContext并将此对象传递到异步任务中。publicstaticvoidDoWork(){//OnUIthreadvarsc=SynchronizationContext.Current;ThreadPool.QueueUserWorkItem(delegate{//...asynctask:doworkonThreadPoolsc.Post(delegate{//doworkontheoriginalcontext(UI)},null);});}SynchronizationContext表示代码运行的是Thread环境,在异步编程中,使用该对象来切换代码执行环境。不同的.NET框架由于其独特的线程切换场景,具有不同的SynchronizationContext子类(覆盖父类虚方法):ASP.NET有AspNetSynchronizationContextWinForm有WindowsFormSynchronizationContextWPF有DispatcherSynchronizationContextASP.NETCore和控制台程序没有SynchronizationContext,SynchronizationContext.Current=nullAspNetSynchronizationContext维护HttpContext.Current、用户身份和文化,但在ASP中。NETCore,这些信息自然依赖于注入,所以不再需要SynchronizationContext;另一个好处是不再获取同步上下文也是性能上的提升。因此,对于ASP.NETCore程序,ConfigureAwait(false)不是必需的。但是,最好在基础库中使用ConfigureAwait(false),因为你不确定上层会不会混合同步/异步代码。3.引入代码为什么会出现死锁观察引入代码,当控件返回上层调用函数时,执行流程使用Result/(Wait方法)等待任务结果:Result/Wait()导致调用线程同步阻塞(等待任务完成),而异步任务执行完成后,会尝试使用捕获的同步上下文执行后续代码,从而形成死锁。正因为如此,我们提出了两种解决死锁的方法:原调用函数一直使用await方法,让调用线程异步等待任务完成,后续代码可以在线程同步上下文中执行。将ConfigureAwait(false)方法应用到异步任务ConfigureAwait(bool):true表示尝试在捕获的原始调用线程SynchronizationContext中执行后续代码;false不再尝试在捕获的线程SynchronizationContext中执行后续代码。ConfigureAwait(false)可以解决调用线程同步阻塞导致的死锁,但是同步阻塞并没有发挥异步编程的优势,所以不推荐使用。这两种解决死锁的方式归根到底都是针对SynchronizationContext的;ASP。NETCore和控制台程序,因为捕获到的SynchronizationContext=null,会选择一个线程同步上下文执行,不会出现死锁。综上所述,微软为了加快开发效率,真的是下了一番功夫。.NET提供的await/async语法糖简化了异步编程方式。在异步编程中,SynchronizationContext决定了后续代码执行的环境,深入了解这个对象的背景和不同框架的实现,可以帮助我们避免编写死锁代码。