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

hxzsmxv2  于 12个月前  发布在  .NET
关注(0)|答案(1)|浏览(123)

最近,我们的旧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和更高版本似乎工作得很好。

ukqbszuj

ukqbszuj1#

我已确认问题与await和/或ConfigureAwait(false)有关。
简化的可再现案例是这样的:

System.Web.Hosting.HostingEnvironment.QueueBackgroundWorkItem(async () => {
    Log(SyncrhonizationContext.Current?.GetType()?.Name);
    var mail = await LoadMailMessage();
 
    ...
    Log(SyncrhonizationContext.Current?.GetType()?.Name);
    await smtpClient.SendMailAsync(mail);
});

字符串
在第一次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库有关。
解决办法似乎是:

  • 在没有设置SynchronizationContext的进程中运行(可以使用ASP.net核心)
  • 设置默认的SynchronizationContext,不要在任何地方使用ConfigureAwait [NOT ABLE TO VERIFY]
  • 在使用ConfigureAwait后,总是将SynchronizationContext设置为null(确认它可以工作,但-认真的吗?)

相关问题