SmtpClient.SendMailAsync随机给出异步操作错误

问题描述 投票:0回答:1

我们的旧版 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
,但会看看会发生什么。

c# asp.net asynchronous webforms
1个回答
0
投票

我已确认问题出在

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 无论如何都会引发异常。

解决方案似乎是:

  • 在没有任何内容设置 SynchronizationContext 的进程中运行
  • 不要在任何地方使用ConfigureAwait
  • 使用ConfigureAwait后始终将SynchronizationContext设置为null(认真的吗?)
© www.soinside.com 2019 - 2024. All rights reserved.