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

6
我有一个控制台应用程序,可以向不同的收件人发送定制的电子邮件(带有附件),我想要并发地发送它们。为了实现这一点,我需要创建单独的SmtpClients,因此我使用QueueUserWorkItem在单独的线程中创建并发送电子邮件。
代码片段如下:

Snippet

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();
}

你能否创建一个简短但完整的程序来展示问题? - Lasse V. Karlsen
为什么不能使用SendAsync并仅处理完成事件,以便您知道是否已发送所有电子邮件? - Brian
我认为你错过了一些代码。我没有看到任何调用send的地方。 - ChaosPandion
@Kiquenet,你不是认真地希望我发布完整的应用程序代码吧?我在问题中发布的代码和下面的答案足以让你继续了解吧? - James
仅发送电子邮件过程,如果可能的话,不是完整的应用程序,就像这个链接中所示:https://dev59.com/uHRB5IYBdhLWcg3w-789#1687178。我想比较样本,以找到更清晰、更优雅的代码。 - Kiquenet
显示剩余2条评论
4个回答

4
你的代码让我困扰的一件事情就是你在线程方法内部调用了events.Add函数。 Dictionary<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实例之间共享一些非线程安全的内容(可能是单例模式或静态声明的内容)。


但是如果每个线程都被正确地发出信号,否则我的应用程序将永远等待,对吧? - James
如果WaitAll在所有线程都有机会将其事件添加到字典之前就发生了,那么它就不起作用了。这就是为什么在等待的同一线程中初始化列表非常重要的原因。 - Aaronaught
啊,非常好的观点!我认为你可能说中了要害。 - James
你试过更新版本了吗?我有一种感觉,字典访问可能是问题的一部分,它不是线程安全的。在前台线程中构建事件列表,完全放弃字典,你应该没问题了。 - Aaronaught
非常准确,这似乎解决了问题。感谢您的时间。 - James
闭包的巧妙使用,你可以做的一件事是将delegate缩短为o => - ChaosPandion

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());

1
或者ThreadPool.QueueUserWorkItem((state) =>{ events[(Guid)state].Set();}, id); - Stan R.
我这样做是为了不必问他是否有.NET 3.5。 - ChaosPandion
我想声明一下,我确实有3.5版本,现在只是在尝试你的解决方案,之后会回复大家! - James
同样的结果,这次有一封电子邮件没有到达邮箱(我多等了一会儿),然后添加线程休眠就可以了! - James
为我分解一下,是“有效的”还是“无效的”。 - ChaosPandion
显示剩余4条评论

2
其不起作用的原因在于当他执行events.Values.ToArray()时,并没有执行所有排队的委托,因此并没有将所有AutoResetEvent实例添加到字典中
当您对Values属性调用ToArray()时,您只会得到已经添加的那些ARE实例!
这意味着在阻塞线程继续之前,您只会等待少量电子邮件同步发送。其余的电子邮件需要由线程池线程处理。
有一种更好的方法,但是这是一种hack当您最终想要阻塞调用线程时,异步执行某些操作似乎毫无意义...
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. 尽快发送

我会创建以下应用程序:

从文本文件中加载电子邮件(File.ReadAllLines)。接下来,创建2 *(# of CPU cores)线程。确定每个线程要处理的行数;即通过将行数(每行addy)除以线程数并四舍五入来计算。接下来,将每个线程设置为完成其地址列表的任务(使用Skip(int)。Take(int)来分配行),并同步地发送每个电子邮件。每个线程都会创建并使用自己的SmtpClient。当每个线程完成时,它会递增存储在共享位置中的一个int。当该int等于线程数时,我知道所有线程都已完成。主控制台线程将不断检查此数字是否相等,并在检查之前Sleep()一定长度的时间。

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


我知道,这几乎没有意义。当你想要阻塞时,为什么要异步执行呢? - user1228
我希望警报尽快发送,而且我只希望应用程序等待所有警报都已发送。如果应用程序结束得太快,那么一些警报就无法发送,这就是我需要等待的原因。 - James
@Will...这就是我一直在做的事情,看看我的原始帖子。我正在使用单独的线程发送电子邮件。 - James
@Will:就是这样。Send 方法可能会阻塞一两秒钟,但大部分时间都是空闲的,只是在网络上发送数据和/或等待邮件服务器的响应。并行发送将提高吞吐量(我认为)。 - Aaronaught
啊,我在使用3.5版本,有什么东西是我可以在这个版本中查看的吗? - James
显示剩余8条评论

0

我曾经遇到过类似的问题(在线程中使用SmtpClient发送邮件,但邮件只会间歇性地到达)。

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

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

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