Windsor 单例线程安全的这种依赖性吗?

Is this dependency of a Windsor singleton thread-safe?

无论如何我都不是异步编程方面的专家,所以我想验证我是否遇到了问题。

我有一个 Web API 应用程序,它使用 Castle Windsor,但也使用内置 HttpConfiguration.Services 管道来实现某些 ASP.NET 功能。在这种情况下,我正在注册一个全局异常处理程序。这是代码:

protected void Application_Start()
{
    //ASP.NET registers this instance in a ConcurrentDictionary and treats it as a singleton
    config.Services.Replace(typeof(IExceptionHandler), container.Resolve<IExceptionHandler>()); 
}

public class EmailExceptionHandler : ExceptionHandler
{
    private readonly SmtpClient client;
    private MailMessage _errorMail;

    public EmailSender(SmtpClient client, MailMessage message) 
        //client automatically resolved with smtp settings pulled from web.config by container. Seems okay to be a singleton here.
        //message automatically resolved with properties like To, From populated from web.config.
        //The intent here is to keep config access out of this class for testability.
    {
        _errorSmtpClient = errorSmtpClient;
        _errorMail = errorMail;
    }

    public override void Handle(ExceptionHandlerContext context)
    {
        // set props on the MailMessage e.g. exception detail

        _errorSmtpClient.SendAsync(_errorMail);

        // standard post-processing, no dependencies necessary
    }
}

public void Install(IWindsorContainer container, IConfigurationStore store)
{
    container.Register(Component.For<SmtpClient>().DependsOn(Dependency.OnAppSettingsValue(/*...*/)));

    container.Register(Component.For<MailMessage>().Named("errorMailMessage")
        .DependsOn(Dependency.OnAppSettingsValue(/*...*/)).LifestyleTransient()); 
        //transient here should bind lifetime to exceptionhandler singleton's lifetime

    container.Register(Component.For<IExceptionHandler>().ImplementedBy<EmailExceptionHandler>()
                        .DependsOn(Dependency.OnComponent("message", "errorMailMessage")));
}

当发生未处理的异常时,ASP.NET 将在其服务字典中查找已注册的 IExceptionHandler 并将错误上下文传递给它。在本例中,这是我在 Windsor 中连接并在应用程序启动时注册的处理程序。

这是调用我定义的句柄覆盖的 .NET Framework 代码:

Task IExceptionHandler.HandleAsync(ExceptionHandlerContext context, CancellationToken cancellationToken)
{
  if (context == null)
    throw new ArgumentNullException("context");
  ExceptionContext exceptionContext = context.ExceptionContext;
  if (!this.ShouldHandle(context))
    return TaskHelpers.Completed();
  return this.HandleAsync(context, cancellationToken);
}

public virtual Task HandleAsync(ExceptionHandlerContext context, CancellationToken cancellationToken)
{
  this.Handle(context);
  return TaskHelpers.Completed();
}

MailMessage 在应用程序启动时被解析并在容器的整个生命周期中持续存在,因为父单例从未被释放。因此,我担心抛出异常的并发请求会导致它们各自的线程进入管理 MailMessage 的代码,可能会使其处于不良状态。

这里的复杂性在于,我不仅要弄清楚是否存在并发线程可以更改 MailMessage 状态的潜在问题,我还必须确保通过管理来解决上述问题由于流的异步性质,线程正确而不会导致死锁。

如果存在问题,我可以想出几种方法来解决它:

像这样:

public override async void Handle(ExceptionHandlerContext context)
{
    await _semaphoreSlim.WaitAsync();
    try
    {
        await _errorSmtpClient.SendMailAsync(_errorMail);
    }
    finally
    {
        _semaphoreSlim.Release();
    }
}

依赖项单例本身都不是线程安全的。

Instance methods of SmtpClient are not thread-safe. This is confirmed in this question。因此,依赖于单个 SmtpClient 的单例不是线程安全的。

Instance methods of MailMessage are also not thread-safe。所以你的单身人士再次不是线程安全的。

此外,我找不到任何迹象表明 MailMessage 旨在可重用。该对象实现 IDisposable 并封装其他也实现 IDisposable 的对象。消息可能包含非托管资源,因此推断 MailMessage 旨在一次性使用并且在发送后应被处置似乎是合乎逻辑的。有关进一步讨论,请参阅 this question

如果您希望仅继续使用单个 MailMessage(可重复使用)和单个 SmtpClient,则需要同步这些对象的所有使用。即便如此,我预计您可能仍会遇到未正确释放非托管资源的问题。

似乎最简单和最安全的 ExceptionHandler 实现是在每次调用时动态构造 MailMessage 和 SmtpClient,然后在传输后处理 MailMessage。