最近,我们的旧ASP.NET Web窗体应用程序开始出现问题。
我们对SmtpClient.sendMailAsync
的调用似乎是随机的,抛出了SmtpException,并显示以下消息:
此时无法启动异步操作。异步操作只能在异步处理程序或模块中启动,或者只能在Page生命周期的某些事件期间启动。如果在执行Page时发生此异常,请确保将Page标记为<%@ Page Async=“true”%>。此异常也可能表示有人试图调用“async void”方法,这在ASP.NET请求处理中通常不受支持。相反,异步方法应该返回一个Task,调用方应该等待它,
我理解错误信息的含义,但是没有一个适用。所有对sendMailAsync
的调用都是在纯async
函数中调用的,以:
System.Web.Hosting.HostingEnvironment.QueueBackgroundWorkItem
字符串
举例来说:
System.Web.Hosting.HostingEnvironment.QueueBackgroundWorkItem(async () => {
smtpClient = new SmtpClient(smtpSection.Host, smtpSection.Port);
smtpClient.Timeout = 10000; // don't block too long.
smtpClient.Credentials = new System.Net.NetworkCredential(smtpSection.Username, smtpSection.Password);
smtpClient.EnableSsl = smtpSection.UseSsl;
...
await smtpClient.SendMailAsync(mail);
});
型
使用ContinueAwait(false)
无法解决此问题。
SynchronizationContext.SetSynchronizationContext(null);
型
在async
函数的开头。
这个错误几乎从来不会在调试或调试阶段出现,只在生产阶段出现,而且在负载较重的情况下会更频繁地出现。对我来说,这表明用于服务后台任务的线程出现了问题。假设它正在获取一个仍然具有UI同步上下文的线程,我添加了SetSynchronizationContext(null)
,但没有用。
显然,sendMailAsync
中有一些代码可以检查潜在的死锁,但它不是基于同步上下文的,我无法判断该检查在做什么。
查看QueueBackgroundWorkItem
的代码,我认为Task.run
的行为并不一定不同,除了在应用回收时可能会被中止。
有没有人知道这是怎么回事?有数百个其他的端点,包括WebAPI 2.0和[WebMethod]类型(页面中ajax调用的静态方法),当我们开始转换到WebAPI时,这个问题变得更加普遍,但实际上只有当我们从[WebMethod]中使用QueueBackgroundWorkItem
时才会出现,而不是在我们转换的任何东西中。
编辑:错误消息的文本表明它是由ASP.NET类生成的,我看到的唯一路径实际上是SynchronizationContext
。尽管我不确定它是如何 Package 为SmtpException
的,因为SmtpClient
源代码似乎没有这样做。(编辑:啊,我们正在从内部异常中获取文本:SmtpException
只是一个“失败”,它用SmtpException
Package 了任何内部异常)
EDIT我的结论是这与ConfigureAwait(false)
有关尽管我的上下文开始时没有任何SynchronizationContext,在调用sendMailAsync
之前的某个时间点使用ConfigureAwait(false)
会导致切换到新的上下文。我可以在每次ConfigureAwait之后显式地清除它,并且可以尝试这样做。sendMailAsync
似乎并不在意我是否使用configureAwait
,但是会看到发生什么
EDIT显然,SynchronizationContext实际上并没有在await
上流动。考虑到暗示它流动的源代码的数量,这是令人惊讶的,但是ExecutionContext有时流动它,有时不流动。请参阅此博客:https://devblogs.microsoft.com/pfxteam/executioncontext-vs-synchronizationcontext/
这基本上是说,mscorlib使用ExecutionContext的方式与其他任何方式都不一样,它使用的内部方法会阻止SynchronizationContext的流动。任何其他使用ExecutionContext.Capture
的方式实际上也会撷取SynchronizationContext,而且会流向以Task.Run
启动的其他执行绪,至少在它们从呼叫端撷取任何内容的情况下是这样。因此,它恢复了一个实际上不属于新线程的SynchronizationContext。我所做的所有测试都有这样的情况,它是非常随机和不可预测的。
这也是只有在.NET Framework下才能做到的。.NET 7和更高版本似乎工作得很好。
1条答案
按热度按时间ukqbszuj1#
我已确认问题与
await
和/或ConfigureAwait(false)
有关。简化的可再现案例是这样的:
字符串
在第一次await之后,SynchronizatinoContext有时会从“null”变为“AspNetSynchronizationContext(可能只有在使用
ConfigureAwait(false)
时才有可能。我无法确保这不是在调用树中设置的)。LoadMailMessage
只是在执行数据库读取,库似乎没有设置SyncCtx(基于对源代码的搜索)。如果SynchonizationContext为null,则执行await
将使用Task的ExecutionContext默认值,在ASP.net进程中,该默认值显然是AspNetSynchronizationContext。这可能只是运气使然,因为哪个线程从泳池里捞出来的水以及它以前的用途使用
ConfigureAwait(false)
肯定没有帮助。如果不设置,可以设置新的SynchronizationContext并保留它,但如果使用ConfigureAwait(false)
,上下文有时会更改。这似乎违反了很多信息,这些信息假定只要您实际上为第一个
ConfigureAwait
调用做了异步工作,那么之后就不需要这样做了。而且,有趣的是,在SendMailAsync上使用
ConfigureAwait(false)
并没有帮助。OperationStart
在上下文上被调用,而不管ConfigureAwait,所以AspNetSynchronizationContext无论如何都会抛出异常。注意:不能用
Task.delay
复制,所以可能与MysqlConnector.net库有关。解决办法似乎是: