我们的旧版 ASP.NET Web 表单应用程序最近开始出现问题。
看似随机,我们对
SmtpClient.sendMailAsync
的调用抛出了带有以下消息的 SmtpException:
此时无法启动异步操作。异步操作只能在异步处理程序或模块内启动,或者在页面生命周期中的某些事件期间启动。 如果在执行页面时发生此异常,请确保该页面被标记为 <%@ Page Async="true" %>。此异常还可能表示尝试调用“async void”方法,该方法在 ASP.NET 请求处理中通常不受支持。相反,异步方法应该返回一个任务,并且调用者应该等待它
我理解错误消息的含义,但它们都不适用。对
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
。虽然,它是如何被包装为 SmtpException
的,但我不确定,因为 SmtpClient
源代码似乎并没有这样做。 (编辑:啊,我们从内部异常获取文本:SmtpException
只是一个“失败”,它用SmtpException
包装任何内部异常)
编辑 我得出的结论是这与
ConfigureAwait(false)
有关
尽管我的上下文一开始没有任何 SynchronizationContext,但在调用 ConfigureAwait(false)
之前的某个时刻使用 sendMailAsync
会导致切换到新上下文。我可以在每次配置等待后明确清除它,并且可以尝试这样做。 sendMailAsync
似乎并不关心我是否使用configureAwait
,但会看看会发生什么。
我已确认问题出在
ConfigureAwait(false)
简化的可重现案例是这样的:
System.Web.Hosting.HostingEnvironment.QueueBackgroundWorkItem(async () => {
Log(SyncrhonizationContext.Current?.GetType()?.Name);
var mail = await LoadMailMessage().ConfigureAwait(false);
...
Log(SyncrhonizationContext.Current?.GetType()?.Name);
await smtpClient.SendMailAsync(mail).ConfigureAwait(false);
});
使用ConfigureAwait调用后,SynchronizatinoContext从“null”更改为“AspNetSychronizationContext”。我不知道为什么它会切换到为 AspNet 设置的线程,但大概线程会返回到池中,无论它们上有什么上下文,因此我们会在其上设置一些随机的 SynchronizationContext 来恢复。
LoadMailMessage
只是进行数据库读取。
这似乎违反了很多信息,这些信息假设只要您实际上为第一个
ConfigureAwait
调用执行异步工作,之后就不需要这样做。
而且,有趣的是,在 SendMailAsync 上使用
ConfigureAwait(false)
并没有帮助。无论配置等待如何,都会在上下文中调用 OperationStart
,因此 AspNetSynchronizationContext 无论如何都会引发异常。
解决方案似乎是: