Использование многопоточности для отправки нескольких писем

Я пытаюсь отправить любое количество писем в кратчайшие сроки с помощью .NET 5.0?

Я играл с чем-то вроде следующего, но я не уверен, оптимально это или даже правильно, поскольку есть ряд элементов, которые я не понимаю.

public async Task SendEmailAsync(string subject, string htmlMessage,
    IEnumerable<string> recipients, string? attachment)
{
    using SemaphoreSlim semaphore = new(10, 10);
    await Task.WhenAll(recipients.Select(async recipient =>
    {
        await semaphore.WaitAsync();

        try
        {
            return SendEmailAsync(subject, htmlMessage, recipient, attachment);
        }
        finally
        {
            semaphore.Release();
        }
    }));
}

Может ли кто-нибудь уточнить, правильно ли это, или сообщить мне, знают ли они лучший подход?

2 ответа
2

Нет, этот код не уважает семафор. Он просто отправляет все электронные письма без ограничения степени параллелизма семафором. Так как return SendEmailAsync возвращается сразу после Task объект получен, а метод не завершен, семафор немедленно освобождается. Таким образом, семафор воспроизводит только запросы на создание, что, как я полагаю, будет быстрым.

Исправление await в try пункт.

public async Task SendEmailAsync(string subject, string htmlMessage,
    IEnumerable<string> recipients, string? attachment)
{
    using SemaphoreSlim semaphore = new(10);
    await Task.WhenAll(recipients.Select(recipient =>
        SendEmailAsync(subject, htmlMessage, recipient, attachment, semaphore)));
}

private async Task SendEmailAsync(string subject, string htmlMessage,
    string recipient, string? attachment, SemaphoreSlim semaphore)
{
    await semaphore.WaitAsync();
    try
    {
        await SendEmailAsync(subject, htmlMessage, recipient, attachment);
    }
    finally
    {
        semaphore.Release();
    }
}

Остальная часть кода мне подходит. Вероятно, я могу только предложить использовать ООП для инкапсуляции данных для массовой рассылки по электронной почте в какой-то класс. Это упростило бы улучшение кода пушнины.

  • Разве это не то же самое, что добавить await ключевое слово перед звонком SendEmailAsync() в моем коде?

    — Джонатан Вуд

  • @JonathanWood, наверное, но ты не можешь вернуться void в Select, верно? Я не силен в синтаксисе лямбда-выражений без IDE, просто написал ответ с мобильного. В любом случае, методоподобный подход по производительности не уступает лямбда-выражению.

    — эспот


  • @JonathanWood попробуйте поменять return к await. Если компиляция прошла успешно, то все готово.

    — эспот

  • 1

    Да, похоже, это работает. Лямбда не обязательно ничего возвращает.

    — Джонатан Вуд

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

Ваш подход также довольно плохо злоупотребляет вызовом Select (). По крайней мере, это делает код очень трудным для чтения.

Вот демонстрация с использованием фиксированного количества задач для имитации отправки электронного письма 1000 получателям. Обратите внимание, что доступ к общей Queue <> не нужно синхронизировать в этом примере, но это может потребоваться в зависимости от вызова API, используемого на практике, поэтому я добавил синхронизацию. Достаточно простой блокировки {}.

private static readonly Random random = new Random();
private static readonly Queue<string> recipients = new Queue<string>();

protected override async Task Run()
{
    for  (int i = 1; i <= 1000; ++i)
    {
        recipients.Enqueue($"recipient_{i:00000}@emaildomain.com");
    }

    List<Task> tasks = new List<Task>();

    for (int i = 1; i <= 50; ++i)
    {
        tasks.Add(SendEmails($"Task {i:00000}"));
    }

    await Task.WhenAll(tasks);
}

private static async Task SendEmails(string taskName)
{
    for (; ;)
    {
        string recipient;

        lock (recipients)
        {
            if (recipients.Count == 0)
            {
                break;
            }

            recipient = recipients.Dequeue();
        }

        Debug.WriteLine($"{taskName}: Sending to {recipient}...");
        await SendEmailAsync(recipient);
        Debug.WriteLine($"{taskName}: Sending to {recipient} complete");
    }

    Debug.WriteLine($"{taskName}: No more recipients; quitting");
}

private static async Task SendEmailAsync(string recipient)
{
    // Simulate sending an email with random network latency.
    await Task.Delay(random.Next(100, 2000));
}

  • Queue<string> не является потокобезопасным и может быть поврежден, если асинхронные вызовы выполняются не на однопоточном SynchronizationContext. все происходит в одном потоке Я не уверен, правда ли это.

    — эспот


  • @aepot хороший момент. В этом примере все происходит в одном потоке, но вы правы, что продолжения могут быть запланированы для разных потоков в зависимости от того, как был реализован API. Отредактирую, чтобы добавить блокировку.

    — гленебоб

  • ConcurrentQueue может быть быстрее, чем lock. Также вы можете обновить текст.

    — эспот


  • 1

    @aepot это, наверное, не имеет значения. Очередь предназначена только для поддержки примера параллелизма задач, который я собрал. OP не может пойти по этому пути. Важная часть — как ограничить количество задач pf без использования SemaphoreSlim.

    — гленебоб

  • 1

    Хорошо, но я не уверен, что отказ от семафора сделает код более эффективным. В любом случае решение мне нравится. Сделаем несколько тестов, чтобы убедиться.

    — эспот

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *