为什么在调用SmtpClient.Send时会周期性地出现“异步调用已经在进行中”的错误?

9

我们有一些(同步)电子邮件代码,创建一个类来创建一个 SmtpClient,并发送电子邮件。 SmtpClient 不会被重用; 然而,我们偶尔会遇到以下异常:

System.Web.HttpUnhandledException (0x80004005): Exception of type 'System.Web.HttpUnhandledException' was thrown. ---> System.InvalidOperationException: An asynchronous call is already in progress. It must be completed or canceled before you can call this method.
   at System.Net.Mail.SmtpClient.Send(MailMessage message)
   at EmailSender.SendMail(MailAddress fromMailAddress, string to, String subject, String body, Boolean highPriority) in ...\EmailSender.cs:line 143

代码长这样:

// ...
var emailSender = new EmailSender();
emailSender.SendMail(toEmail, subject, body, true);
// emailSender not used past this point
// ...

public class EmailSender : IEmailSender
{
    private readonly SmtpClient smtp;

    public EmailSender()
    {
        smtp = new SmtpClient();
    }

    public void SendMail(MailAddress fromMailAddress, string to, string subject, string body, bool highPriority)
    {
        if (fromMailAddress == null)
            throw new Exception();
        if (to == null)
            throw new ArgumentException("No valid recipients were supplied.", "to");

        // Mail initialization
        var mailMsg = new MailMessage
        {
            From = fromMailAddress,
            Subject = subject,
            Body = body,
            IsBodyHtml = true,
            Priority = (highPriority) ? MailPriority.High : MailPriority.Normal
        };

        mailMsg.To.Add(to);


        smtp.Send(mailMsg);
    }
}
2个回答

5
你需要使用Disposeusing或实现可处理模式来处理SmtpClient,在这里更适合实现可处理模式,因为你在构造函数中将SmtpClient的生命周期与EmailSender的生命周期绑定。

这可能会解决此异常。


我同意我们应该Dispose,但它真的会引起这个问题吗? - Danny Tuppeny
可能是的。你问题中的“偶尔”可能与超时、清理等有关。添加正确的资源管理,看看异常是否仍然出现。如果无法重现问题,则很难确定问题所在。 - Emond

1
我的猜测是 SmtpClient 并不适用于同时发送多个消息。
我会将类更改为这样:
public class EmailSender : IEmailSender
{
    Queue<MailMessage> _messages = new Queue<MailMessage>();
    SmtpClient _client = new SmtpClient();

    public EmailSender()
    {
    }

    public void SendMail(MailAddress fromMailAddress, string to, string subject, string body, bool highPriority)
    {
        if (fromMailAddress == null)
            throw new ArgumentNullException("fromMailAddress");
        if (to == null)
            throw new ArgumentException("No valid recipients were supplied.", "to");

        // Mail initialization
        var mailMsg = new MailMessage
        {
            From = fromMailAddress,
            Subject = subject,
            Body = body,
            IsBodyHtml = true,
            Priority = (highPriority) ? MailPriority.High : MailPriority.Normal
        };

        mailMsg.To.Add(to);

        lock (_messages)
        {
            _messages.Enqueue(mailMsg);
            if (_messages.Count == 1)
            {
                ThreadPool.QueueUserWorkItem(SendEmailInternal);
            }
        }
    }

    protected virtual void SendEmailInternal(object state)
    {
        while (true)
        {
            MailMessage msg;
            lock (_messages)
            {
                if (_messages.Count == 0)
                    return;
                msg = _messages.Dequeue();
            }

            _client.Send(msg)
        }
    }
}

由于在构造函数中创建客户端没有任何理由,因此我进行了更改。我还更改了类,如果fromMailAddress为null,则抛出ArgumentNullException而不是Exception。一个空的Exception并没有什么意义。 更新 现在代码使用线程池线程发送电子邮件(并重用smtpclient)。

或者说,不必创建一个新的SmtpClient实例,OP可以只锁定该对象直到消息被发送。 - MeTitus
是的,在构造函数中创建客户端有很好的理由:当您向同一服务器发送多个电子邮件时,连接将被池化。请参见http://msdn.microsoft.com/en-us/library/system.net.mail.smtpclient.dispose.aspx上的备注部分。 - Emond
我不明白为什么我们需要锁。我们没有共享任何一个类的实例。EmailSender类被创建,它的SendEmail方法被调用,然后就完成了。没有任何静态内容。 - Danny Tuppeny
你没有提到不分享的事情,因为这似乎是错误信息的意思。 - jgauffin
我的问题说“SmtpClient未被重复使用”,并且在代码示例中有一条注释“// emailSender not used past this point” :( - Danny Tuppeny

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接