我不是任何方式的异步编程专家,所以我想验证我有问题。
我有一个使用Castle Windsor的Web API应用程序,但也为某些ASP.NET函数使用内置的HttpConfiguration.Services
管道。在这种情况下,我注册了一个全局异常处理程序。这是代码:
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
状态的潜在问题,我必须确保通过管理线程来解决所述问题正确而不会由于流的异步性而导致死锁。
如果问题存在,我可以想出几种解决方法:
围绕邮件设置和发送电子邮件创建锁定声明。由于void方法本身不是异步的,唯一的缺点似乎是导致并发线程阻塞直到它们可以进入。这不会与使用SemaphoreSlim
Wait()
方法相同吗?
创建一个Typed工厂依赖项,并在Handle方法中显式解析MailMessage的一个实例,并将其分配给一个局部变量。
不要一直使用async并调用SemaphoreSlim.WaitAsync()
来阻止其他线程 - 这会有效吗?
像这样:
public override async void Handle(ExceptionHandlerContext context)
{
await _semaphoreSlim.WaitAsync();
try
{
await _errorSmtpClient.SendMailAsync(_errorMail);
}
finally
{
_semaphoreSlim.Release();
}
}
答案 0 :(得分:4)
依赖和单身本身都不是线程安全的。
Instance methods of SmtpClient are not thread-safe。这已在question中得到确认。因此,依赖于单个SmtpClient的单例不是线程安全的。
Instance methods of MailMessage are also not thread-safe。所以你的单身人再次不是线程安全的。
此外,我找不到任何暗示MailMessage可以重复使用的内容。该对象实现了IDisposable并封装了也实现IDisposable的其他对象。消息可以保存非托管资源,因此可以合理地断定MailMessage旨在单独使用,并且应该在发送后进行处理。有关进一步的讨论,请参阅this question。
如果您只想继续使用一个可重复使用的MailMessage,并且只需要一个SmtpClient,则需要同步这些对象的所有使用。即便如此,我仍然希望您可能仍然存在未正确释放非托管资源的问题。
看起来,最简单和最安全的ExceptionHandler实现会在每次调用时动态构造MailMessage和SmtpClient,然后在传输后处理MailMessage。