Отправка писем в отдельных потоках с помощью QueueUserWorkItem - PullRequest
6 голосов
/ 08 января 2010

У меня есть консольное приложение, которое отправляет настроенные электронные письма (с вложениями) разным получателям, и я хочу отправлять их одновременно. Мне нужно создать отдельные 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 секунд, похоже, дает приложению достаточно времени для завершения.

Возможно, это связано с тем, как я жду отправки электронных писем? Или это проблема реального метода отправки? Было ли письмо определенно отправлено из приложения после прохождения этой строки?

Любая мысль о том, что это было бы замечательно, не может показаться, что я могу указать на истинную причину.

Обновление

В соответствии с запросом здесь указан код 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 ]

4 голосов
/ 08 января 2010

Одна из вещей, которая меня беспокоит в вашем коде, это то, что вы вызываете events.Add в методе thread. Класс 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 экземплярами (возможно, одиночным или чем-то объявленным статически).

2 голосов
/ 08 января 2010

Причина, по которой он не работает, заключается в том, что когда он нажимает events.Values.ToArray () , не все делегаты из очереди выполнили , и поэтому не все экземпляры AutoResetEvent были добавлены в словарь .

Когда вы вызываете ToArray () для свойства Values, вы получаете только те экземпляры 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. Отправлено как можно быстрее

Я бы создал следующее приложение:

Загрузка электронных писем из текстового файла (File.ReadAllLines). Затем создайте 2 * (# ядра ЦП) потоков. Определите количество строк для обработки в потоке; то есть разделите количество строк (адди на строку) на количество потоков, округляя их в большую сторону. Затем задайте каждому потоку задачу прохождения его списка адресов (используйте Skip (int) .Take (int) для разделения строк) и Send () синхронно для каждого письма. Каждый поток будет создавать и использовать свой собственный SmtpClient. По завершении каждого потока он увеличивает int, хранящийся в общем расположении. Когда это int равно числу потоков, я знаю, что все потоки завершены. Главный поток консоли будет постоянно проверять это число на равенство и Sleep () в течение установленного промежутка времени, прежде чем проверять его снова.

Это звучит немного глупо, но это будет работать. Вы можете настроить количество потоков, чтобы получить наилучшую пропускную способность для отдельной машины, а затем экстраполировать ее, чтобы определить правильное количество потоков. Есть определенно более изящные способы заблокировать поток консоли до полного завершения, но не так просто.

2 голосов
/ 08 января 2010

Вы, вероятно, хотите сделать это ...

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());
0 голосов
/ 04 ноября 2014

У меня была похожая проблема (использование SmtpClient из потока, и электронные письма приходят только периодически).

Изначально класс, отправляющий электронные письма, создал один экземпляр SmtpClient.Проблема была решена путем изменения кода для создания нового экземпляра SmtpClient каждый раз, когда необходимо отправить электронное письмо и с утилитой SmtpClient (с помощью оператора using).

 SmtpClient smtpClient = InitSMTPClient();
 using (smtpClient)
 {
    MailMessage mail = new MailMessage();
    ...
    smtpClient.Send(mail);
 }
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...