# Using multithreading to send multiple emails

I'm trying to send any number of emails in the shortest amount of time using .NET 5.0?

I've been playing with something like the following but I am not sure if it is optimal or even correct as there are a number of elements I don't understand.

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

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


Can someone clarify if this is correct, or let me know if they know a better approach?

• To the best of your knowledge, does the code work as intended? If not, then the post is off-topic - note that the help page states "If you are looking for feedback on a specific working piece of code...then you are in the right place!" Jul 8 at 22:40
• What makes you think the email client can handle parallel requests? I’d assume they are all just queued up and processed sequentially there anyway. Jul 8 at 23:55
• @Aganju: Most of the time it takes to send an email is spent waiting for the server to respond. It would be stupid to queue them all up. At any rate, I'm not responsible for if mail server designers make stupid designs. I'm just responsible that my code doesn't have a stupid design. Jul 9 at 1:18

Nope, this code doesn't respect the semaphore. It just send all emails without concurrency degree limiting by semaphore. Because return SendEmailAsync returns right after Task object is received not the method finished, then semaphore is released immediately. Thus semaphore thottles only requests creation which I assume is fast.

The fix is await in try clause.

public async Task SendEmailAsync(string subject, string htmlMessage,
IEnumerable<string> recipients, string? attachment)
{
using SemaphoreSlim semaphore = new(10);
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();
}
}


The rest part of the code looks fine for me. Probably I can only suggest to use OOP to encapsulate data for bulk emailing into some class. It would make furure code improvements easier.

• Would this not be the same as adding the await keyword before calling SendEmailAsync() in my code? Jul 8 at 23:02
• @JonathanWood probably but you can't return void in Select, right? I'm not strong in lambdas syntax without IDE, just wrote the answer from mobile. Anyway, the method-like approach isn't slower than lambda in performance. Jul 8 at 23:08
• @JonathanWood try changing return to await. If it will compile successful then it's done. Jul 8 at 23:14
• Yes, that seems to work. A lambda doesn't necessarily return anything. Jul 8 at 23:17

Your approach can result in a huge number of tasks active at any given time, each of them representing load on the machine's IO resources. You need a way to limit the number of tasks.

Your approach also abuses the Select() call pretty badly. If nothing else, it makes the code very hard to read.

Here is a demonstration using a fixed number of tasks to simulate sending an email to 1000 recipients. Note that access to the shared Queue<> does not need to be synchronized in this example, but it may be needed depending on the API call used in practice, so I added synchronization. A simple lock {} suffices.

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

{
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}"));
}

}

{
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");
}


• Queue<string> isn't thread-safe and might be damaged if async calls performed not on single-threaded SynchronizationContext. everything happens on one thread I'm not sure if that's true. Jul 8 at 23:01
• ConcurrentQueue may be faster than lock. Also you may update the text. Jul 8 at 23:16