从Parallel.ForEach循环内部调用异步方法时无效的操作异常

时间:2011-09-15 16:15:19

标签: c# c#-4.0 windows-services

我继承了一个处理队列中大量电子邮件的Windows服务。听起来很简单,Grab队列,发送电子邮件,如果SmtpClient.SendAsync没有从回调中返回错误然后将数据库中的电子邮件标记为正在发送..我在线程上使用信号量来等待多个可以调用SMTP客户端的Async Send方法。这是我可以获得状态的唯一方法,并且每个Microsoft文档必须完成操作才能使另一个调用异步。所以现在为有趣的部分。我决定使用Parallel.ForEach让他像这样排队。在Windows Service OnStart中调用此方法。请注意我已尝试在单独的线程上调用此方法并获得相同的结果。

我在想A,我错过了一些明显的东西,因为我对线程缺乏了解,或者有些东西是扁平的。很可能是A.

 private static void ProcessEmailQueue()
    {
        List<EmailQueue> emailQueue =
            _repository.Select<EmailQueue>().Where(x => x.EmailStatuses.EmailStatus == "Pending").ToList();
        Parallel.ForEach(emailQueue, message =>
                                         {
                                             _smtpMail.FromAddress = message.FromAddress;
                                             _smtpMail.ToAddress = message.ToAddress;
                                             _smtpMail.Subject = message.Subject;
                                             _smtpMail.SendAsHtml = message.IsHtml > 0;
                                             _smtpMail.MessageBody = message.MessageBody;
                                             _smtpMail.UserToken = message.EmailQueueID;
                                             bool sendStatus = _smtpMail.SendMessage();
                                                 // THIS BLOWS UP with InvalidOperation Exception
                                         });
    }

以下是使用循环调用的SMTP方法。

public bool SendMessage()
    {
        mailSendSemaphore = new Semaphore(0, 10); // This is defined as  private static Semaphore mailSendSemaphore;
        try
        {
            var fromAddress = new MailAddress(FromAddress);
            var toAddress = new MailAddress(ToAddress);

            using (var mailMessage = new MailMessage(fromAddress, toAddress))
            {
                mailMessage.Subject = Subject;
                mailMessage.IsBodyHtml = SendAsHtml;
                mailMessage.Body = MessageBody;
                Envelope = mailMessage;
                smtp.SendCompleted += smtp_SendCompleted;
                smtp.SendAsync(mailMessage, UserToken);
                mailSendSemaphore.WaitOne();
                return _mailSent;
            }
        }
        catch (Exception exception)
        {
            _logger.Error(exception);

            return _mailSent;
        }
    }

用于Smtp发送的回叫

 private void smtp_SendCompleted(object sender, AsyncCompletedEventArgs e)
    {
        if (e.Cancelled)
        {
        }
        if (e.Error != null)
        {
        }
        else
        {
            _mailSent = true;
        }
        mailSendSemaphore.Release(2);
    }

这是Exception,由于一些奇怪的原因,我花了一些时间来解决它。

System.InvalidOperationException was unhandled by user code

消息=异步调用已在进行中。必须先完成或取消它才能调用此方法。   来源=系统   堆栈跟踪:        在System.Net.Mail.SmtpClient.SendAsync(MailMessage消息,对象userToken)        在SmtpMail.cs中的DFW.Infrastructure.Communications.SmtpMail.SendMessage():第71行        在Service1.cs中的EmaiProcessorService.EmailQueueService.b_ 0(EmailQueue消息):第57行        在System.Threading.Tasks.Parallel。&lt;&gt; c _DisplayClass2d 2.<ForEachWorker>b__23(Int32 i) at System.Threading.Tasks.Parallel.<>c__DisplayClassf 1.b__c()   InnerException:

似乎我的waitone被System.Threading.Tasks.Parallel消灭了

2 个答案:

答案 0 :(得分:2)

好的,现在我们已经收到了错误文本,看起来很清楚:

  

消息=异步调用已在进行中。必须先完成或取消,然后才能调用此方法。

这与documentation

一致

两个简单的选择:

  • 创建固定数量的客户端和要发送的消息队列。使每个客户端在每次完成时从队列中获取消息,直到队列为空。 BlockingCollection<T>对此有好处。

  • 为每条消息创建一个新的SmtpClient。这可能会导致您在SMTP服务器上有效地启动DOS攻击,这是不理想的。

老实说,当你等待邮件被发送时,为什么你使用SendAsync并不是很清楚......

答案 1 :(得分:1)

我不清楚为什么你在这里使用信号量,但你几乎肯定不正确地使用它。您正在为SendMessage的每次调用创建一个新的信号量实例。此外,您在其上调用WaitOne一次,然后调用Release(2),因此最终您将拥有比获取更多的版本。这可能是导致InvalidOperationException

的原因

并行处理电子邮件队列对您没有任何好处,因为您一次只能发送一条消息。并试图在Parallel.Foreach内异步进行,这只是更加不必要的并发症。

你最好使用像ThreadPool.QueueUserWorkItem这样的东西,并且有一个简单的循环,一次发送一条消息。

List<EmailQueue> emailQueue =
    _repository.Select<EmailQueue>().Where(x => x.EmailStatuses.EmailStatus == "Pending").ToList();

ThreadPool.QueueUserWorkItem(ProcessEmailQueue, emailQueue);

void ProcessEmailQueue(object state)
{
    List<EmailQueue> emailQueue = (List<EmailQueue>)state;
    foreach (var message in EmailQueue)
    {
        // Format and send message here.
    }
}

或者,您可以使用Task执行相同的操作。关键是你只需要一个线程来顺序处理队列。由于您不能一次发送多条消息,Parallel.ForEach对您没有任何好处。

编辑:

如果您需要一次执行多个发送,则可以修改原始代码。首先,在类范围初始化信号量:

private static Semaphore mailSendSemaphore = new Semaphore(10, 10);

然后,在您的SendMessage方法中:

bool SendMessage()
{
    // acquire semaphore. This will block until there's a slot available.
    mailSendSemaphore.WaitOne();
    try
    {
        // do all your processing here, including sending the message.
        // use Send rather than SendAsync
    }
    finally
    {
        mailSendSemaphore.Release();
    }
}

无需使用SendAsync