当前位置: 首页 > 编程语言 > C#

使用ContinueWith或Async-Await时的不同行为分享

时间:2023-04-10 21:48:15 C#

使用ContinueWith或Async-Await时的不同行为导致死锁。用t.ContinueWith替换async-await方法,它将正常工作。为什么?publicclassMyFilter:ActionFilterAttribute{publicoverridevoidOnActionExecuting(ActionExecutingContextfilterContext){varuser=_authService.GetUserAsync(username).Result;}}publicclassAuthService:IAuthService{publicasyncTaskGetUserAsync(stringusername){r_http=CliwaGetStringAsync(url).ConfigureAwait(false);返回awaitJsonConvert.DeserializeObjectAsync(jsonUsr);这有效:publicclassHttpClientWrapper:IHttpClient{publicTaskGetStringAsync(stringurl){return_client.GetStringAsync(url).Continue=>{_log.InfoFormat("Response:{0}",url);返回结果;});}这段代码会死锁:_log.InfoFormat("响应:{0}",url);返回结果;我已经在我的博客和最近的一篇MSDN文章中描述了这种死锁行为。因此,当HTTP请求完成时,它会尝试进入SynchronizationContext以运行InfoFormat。但是,SynchronizationContext已经存在一个线程-线程在Result上阻塞(等待异步方法完成)。另一方面,ContinueWith的默认行为是默认安排它继续使用当前的TaskScheduler(在本例中为线程池TaskScheduler)。正如其他人指出的那样,最好“始终”使用await,即不要阻止异步代码。不幸的是,在这种情况下这不是一个选项,因为MVC不支持过滤器的异步操作(作为旁注,请在此处投票支持此支持)。所以你可以选择使用ConfigureAwait(false)或者只使用同步方法。在这种情况下,我建议使用同步方法。ConfigureAwait(false)仅在它适用的任务尚未完成时才起作用,所以我建议一旦你使用ConfigureAwait(false),你应该为那个点之后的方法中的每个等待使用它(在这种情况下,每个方法调用堆栈)。如果出于效率原因使用ConfigureAwait(false),那么这很好(因为它在技术上是可选的)。在这种情况下,出于正确的原因必须使用ConfigureAwait(false),因此IMO造成了维护负担。同步方法会更清楚。解释你为什么死锁等待你的第一行:varuser=_authService.GetUserAsync(username).Result;在等待GetUserAsync的结果时阻塞线程和当前上下文。使用await时,它会尝试在任务await完成后运行原始上下文中的所有剩余语句,如果原始上下文被阻塞(这是因为.Result),则会导致死锁。看起来您正试图通过在GetUserAsync中使用.ConfigureAwait(false)来抢占它,但是当await生效时,为时已晚,因为首先遇到了另一个await。实际执行路径如下:_authService.GetUserAsync(username)_httpClientWrp.GetStringAsync(url)//其实还没有等待(需要有结果才能等待)await_client.GetStringAsync(url)//这里是第一个await生效当_client.GetStringAsync完成时,其余代码无法在原始上下文中继续,因为该上下文已被阻止。为什么ContinueWith的行为不同ContinueWith不会尝试在原始上下文中运行其他块(除非您使用其他参数告诉它),因此它不会遇到此问题。这是您注意到的行为差异。异步解决方案如果您仍然想使用异步而不是ContinueWith,您可以将.ConfigureAwait(false)添加到第一个遇到的异步:stringresult=await_client.GetStringAsync(url).ConfigureAwait(false);您可能已经知道,告诉await不要尝试在原始上下文中运行其余代码。注意以后在使用async/await时尽量不要使用阻塞方法。请参阅Preventingdeadlockswhencallingasynchronousmethodswithoutawait以避免将来出现此问题。当然,我的回答只是部分的,但我还是会继续。您的Task.ContinueWith(...)调用未指定调度程序,因此将使用TaskScheduler.Current-无论当时是什么。但是,等待片段将在等待任务完成时在捕获的上下文中运行,因此这两段代码可能会或可能不会产生类似的行为-取决于TaskScheduler.Current的值。例如,如果您的第一个片段是直接从UI代码调用的(在这种情况下,TaskScheduler.Current==TaskScheduler.Default),那么延续(记录代码)将在默认的TaskScheduler上执行-即在线程池执行上。但是,在第二个片段中,无论是否对GetStringAsync返回的任务使用ConfigureAwait(false),延续(日志记录)实际上将在UI线程上运行。在等待调用GetStringAsync之后,ConfigureAwait(false)只会影响代码的执行。这是另一个例子:privateasyncvoidForm1_Load(objectsender,EventArgse){awaitthis.Blah().ConfigureAwait(false);//InvalidOperationException在这里。this.Text="哦不,我不在UI线程上了。";}privateasyncTaskBlah(){awaitTask.Delay(1000);this.Text="嗨,我在UI线程上。";给定的代码在Blah()中设置Text就好了,但它在Load处理程序的延续中抛出跨线程异常。我发现这里发布的其他解决方案在ASP.NETMVC5上不起作用,它仍然使用同步操作过滤器。发布的解决方案不保证将使用新线程,它们只是指定不必使用相同的线程。我的解决方案是使用Task.Factory.StartNew()并在方法调用中指定TaskCreationOptions.LongRunning。这确保始终使用新的/不同的线程,因此您可以放心,您永远不会死锁。因此,使用OP示例,以下是对我有用的解决方案://保证将使用新的/不同的线程来执行封闭操作//避免调用线程死锁varuser=Task.Factory.StartNew(()=>_authService.GetUserAsync(username).Result,TaskCreationOptions.LongRunning)。结果;以上就是C#学习教程:使用ContinueWith或Async-Await时不同行为分享的全部内容,如果对你有用,需要进一步了解C#学习教程,希望大家多多关注——本文整理自网络,不代表立场。侵权请点击右侧联系管理员删除。如需转载请注明出处: