使用QueueUserWorkItem在单独的线程上发送电子邮件

时间:2010-01-08 19:26:28

标签: c# multithreading smtp

我有一个控制台应用程序,可以将定制的电子邮件(附件)发送给不同的收件人,我想同时发送它们。我需要创建单独的SmtpClients来实现这一点,所以我使用QueueUserWorkItem来创建电子邮件并将它们发送到不同的线程中。

摘录

var events = new Dictionary<Guid, AutoResetEvent>();
foreach (...)
{
    ThreadPool.QueueUserWorkItem(delegate
    {
        var id = Guid.NewGuid();
        events.Add(id, new AutoResetEvent(false));
        var alert = // create custom class which internally creates SmtpClient & Mail Message
        alert.Send();
        events[id].Set();
    });   
}
// wait for all emails to signal
WaitHandle.WaitAll(events.Values.ToArray());

我已经注意到(间歇性地)有时并非所有电子邮件都使用上述代码到达特定邮箱。我原以为使用Send而不是SendAsync意味着电子邮件已经从应用程序中发送出来。但是,在WaitHandle.WaitAll行之后添加以下代码行:

System.Threading.Thread.Sleep(5000);

似乎工作。我的想法是,无论出于何种原因,仍然没有发送一些电子邮件(即使在Send方法运行之后)。给予额外的5秒似乎给应用程序足够的时间来完成。

这可能是我等待发送电子邮件的方式的问题吗?或者这是实际Send方法的问题?一旦我们通过这条线,电子邮件肯定是从应用程序发送的吗?

任何有关此问题的想法都会很棒,似乎无法确切地指出实际原因。

更新

这里要求的是SMTP代码:

SmtpClient client = new SmtpClient("Host");
FieldInfo transport = client.GetType().GetField("transport", BindingFlags.NonPublic | BindingFlags.Instance);
FieldInfo authModules = transport.GetValue(client).GetType()
    .GetField("authenticationModules", BindingFlags.NonPublic | BindingFlags.Instance);
Array modulesArray = authModules.GetValue(transport.GetValue(client)) as Array;
modulesArray.SetValue(modulesArray.GetValue(2), 0);
modulesArray.SetValue(modulesArray.GetValue(2), 1);
modulesArray.SetValue(modulesArray.GetValue(2), 3);
try
{
    // create mail message
    ...
    emailClient.Send(emailAlert);
}
catch (Exception ex)
{
    // log exception
}
finally
{
    emailAlert.Dispose();
}

4 个答案:

答案 0 :(得分:4)

令我烦恼的一件事就是你在线程方法中调用events.AddDictionary<TKey, TValue>类不是线程安全的;这段代码不应该在线程内。

更新:我认为ChaosPandion发布了一个很好的实现,但我会让它变得更简单,因此在线程安全方面没有什么可能可能出错:

var events = new List<AutoResetEvent>();
foreach (...)
{
    var evt = new AutoResetEvent();
    events.Add(evt);
    var alert = CreateAlert(...);
    ThreadPool.QueueUserWorkItem(delegate
    {           
        alert.Send();
        evt.Set();
    });
}
// wait for all emails to signal
WaitHandle.WaitAll(events.ToArray());

我在这里完全删除了字典,并且所有AutoResetEvent个实例都在同一个线程中创建,后来执行WaitAll。如果此代码不起作用,那么它必须是电子邮件本身的问题;要么服务器正在丢弃消息(你发送了多少消息?),要么你试图在Alert实例之间共享一些非线程安全的东西(可能是单例或静态声明的东西)。

答案 1 :(得分:2)

你可能想要这样做......

var events = new Dictionary<Guid, AutoResetEvent>();
foreach (...)
{
    var id = Guid.NewGuid();
    events.Add(id, new AutoResetEvent(false));
    ThreadPool.QueueUserWorkItem((state) =>
    {           
        // Send Email
        events[(Guid)state].Set();
    }, id);   
}
// wait for all emails to signal
WaitHandle.WaitAll(events.Values.ToArray());

答案 2 :(得分:2)

它无法工作的原因是当他遇到events.Values.ToArray()并非所有排队的代理都已执行因此并非所有AutoResetEvent实例都已添加到字典

在Values属性上调用ToArray()时,只会获得已添加的ARE实例!

这意味着您将等待只有少数电子邮件在被阻止的线程继续之前同步发送。其余的电子邮件尚未由ThreadPool线程处理。

有一种更好的方法,但是这是一个黑客攻击当你想在最后阻止调用线程时,异步做某事似乎毫无意义......

var doneLol = new AutoResetEvent();

ThreadPool.QueueUserWorkItem(
delegate
{
  foreach (...)
  {
    var id = Guid.NewGuid();
    var alert = HurrDurr.CreateAlert(...);
    alert.Send();
  }
  doneLol.Set();
});   

doneLol.WaitOne();

好的,考虑到以下要求:

  1. 控制台应用
  2. 很多电子邮件
  3. 尽快发送
  4. 我创建了以下应用程序:

    从文本文件(File.ReadAllLines)加载电子邮件。接下来,创建2 *(CPU核心数)线程。确定每个线程要处理的行数;即,将线的数量(每行的addy)除以线程数,向上舍入。接下来,将每个线程设置为遍历其地址列表的任务(使用Skip(int).Take(int)来分割行)和Send()同步发送每个电子邮件。每个线程都会创建并使用自己的SmtpClient。当每个线程完成时,它会增加存储在共享位置的int。当int等于线程数时,我知道所有线程都已完成。在再次检查之前,主控制台线程将持续检查此数字是否相等以及Sleep()一段时间。

    这听起来有点笨拙,但它会起作用。您可以调整线程数以获得单个计算机的最佳吞吐量,然后从中进行推断以确定正确的线程数。肯定有更优雅的方法来阻止控制台线程直到完成,但没有一个简单。

答案 3 :(得分:0)

我遇到了类似的问题(使用线程中的SmtpClient,电子邮件只是间歇性地到达)。

最初发送电子邮件的类创建了一个SmtpClient实例。每次需要发送电子邮件处理SmtpClient(使用using语句)时,通过更改代码来创建SmtpClient的新实例来解决问题。

 SmtpClient smtpClient = InitSMTPClient();
 using (smtpClient)
 {
    MailMessage mail = new MailMessage();
    ...
    smtpClient.Send(mail);
 }