抛出特定exception时,SmtpClient.SendMailAsync会导致死锁

我正在尝试基于VS2013项目模板中的示例AccountController为ASP.NET MVC5网站设置电子邮件确认。 我已经使用SmtpClient实现了IIdentityMessageService ,试图让它尽可能简单:

 public class EmailService : IIdentityMessageService { public async Task SendAsync(IdentityMessage message) { using(var client = new SmtpClient()) { var mailMessage = new MailMessage("some.guy@company.com", message.Destination, message.Subject, message.Body); await client.SendMailAsync(mailMessage); } } } 

调用它的控制器代码直接来自模板(由于我想排除其他可能的原因,因此将其解压缩为单独的操作):

 public async Task TestAsyncEmail() { Guid userId = User.Identity.GetUserId(); string code = await UserManager.GenerateEmailConfirmationTokenAsync(userId); var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = userId, code = code }, protocol: Request.Url.Scheme); await UserManager.SendEmailAsync(userId, "Confirm your account", "Please confirm your account by clicking here"); return View(); } 

但是,当邮件无法发送时,我会遇到奇怪的行为,但只有在一个特定的实例中,当主机无法访问时。 示例配置:

        

在这种情况下,请求似乎死锁,永远不会向客户端返回任何内容。 如果邮件由于任何其他原因而无法发送(例如主机主动拒绝连接),则会正常处理exception并获得YSOD。

查看Windows事件日志,似乎在同一时间范围内抛出InvalidOperationException ,并显示消息“异步模块或处理程序在异步操作仍未完成时完成。”; 如果我尝试在控制器中捕获SmtpException并在catch块中返回ViewResult ,那么我在YSOD中得到相同的消息。 所以我认为在任何一种情况下await -ed操作都无法完成。

据我所知,我遵循SO上其他post中概述的所有async / await最佳实践(例如HttpClient.GetAsync(…)在使用await / async时永远不会返回 ),主要是“使用async / await all上升的方式“。 我也尝试过使用ConfigureAwait(false) ,没有任何变化。 由于代码仅在抛出特定exception时才会死锁,因此我认为一般模式在大多数情况下都是正确的,但在内部发生的某些事情使得它在这种情况下不正确; 但由于我对并发编程很陌生,我觉得我可能错了。

有什么我做错了吗? 我总是可以在SendAsync方法中使用同步调用(即SmtpClient.Send() ),但感觉这应该按原样工作。

试试这个实现,只需使用client.SendMailExAsync而不是client.SendMailAsync 。 如果它有任何区别,请告诉我们:

 public static class SendMailEx { public static Task SendMailExAsync( this System.Net.Mail.SmtpClient @this, System.Net.Mail.MailMessage message, CancellationToken token = default(CancellationToken)) { // use Task.Run to negate SynchronizationContext return Task.Run(() => SendMailExImplAsync(@this, message, token)); } private static async Task SendMailExImplAsync( System.Net.Mail.SmtpClient client, System.Net.Mail.MailMessage message, CancellationToken token) { token.ThrowIfCancellationRequested(); var tcs = new TaskCompletionSource(); System.Net.Mail.SendCompletedEventHandler handler = null; Action unsubscribe = () => client.SendCompleted -= handler; handler = async (s, e) => { unsubscribe(); // a hack to complete the handler asynchronously await Task.Yield(); if (e.UserState != tcs) tcs.TrySetException(new InvalidOperationException("Unexpected UserState")); else if (e.Cancelled) tcs.TrySetCanceled(); else if (e.Error != null) tcs.TrySetException(e.Error); else tcs.TrySetResult(true); }; client.SendCompleted += handler; try { client.SendAsync(message, tcs); using (token.Register(() => client.SendAsyncCancel(), useSynchronizationContext: false)) { await tcs.Task; } } finally { unsubscribe(); } } }