我继承了一个处理队列中大量电子邮件的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消灭了
答案 0 :(得分:2)
好的,现在我们已经收到了错误文本,看起来很清楚:
一致消息=异步调用已在进行中。必须先完成或取消,然后才能调用此方法。
两个简单的选择:
创建固定数量的客户端和要发送的消息队列。使每个客户端在每次完成时从队列中获取消息,直到队列为空。 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
。