Given the fairly high frequency of sending email, it is likely that you are scheduling too many Tasks
for the Scheduler.
In Method 1, calling Task.Run
will create a new task each time, each of which needs to be scheduled on a thread. It is quite likely that you are exhausting your thread pool by doing this.
Although Method 2 will be less Task hungry, even with unawaited Task
invocation (fire and forget), the completion of the continuation after the async method will still need to be scheduled on the Threadpool, which will adversely affect your system.
Instead of unawaited Tasks
or Task.Run
, and since you are a Windows Service, I would instead have a long-running background thread dedicated to sending emails. This thread can work independently to your primary work, and emails can be scheduled to this thread via a queue.
If a single mail sending thread is insufficient to keep pace with the mails, you can extend the number of EmailSender
threads, but constrain this to a reasonable, finite number).
You should explore other optimizations too, which again will improve the throughput of your email sender e.g.
- Can the email senders keep long lived connections to the mail server?
- Does the mail server accept batches of email?
Here's an example using BlockingCollection with a backing ConcurrentQueue
of your email message Model.
- Creating a queue which is shared between the producer "PrimaryWork" thread and the "EmailConsumer" thread (obviously, if you have an IoC container, it's best registered there)
- Enqueuing mails on the primary work thread
- The consumer
EmailSender
runs a loop on the blocking collection queue until CompleteAdding
is called
- I've used a
TaskCompletionSource
to provide a Task which will complete once all messages have been sent, i.e. so that graceful exit is possible without losing emails still in the queue.
public class PrimaryWork
{
private readonly BlockingCollection<EmailModel> _enqueuer;
public PrimaryWork(BlockingCollection<EmailModel> enqueuer)
{
_enqueuer = enqueuer;
}
public void DoWork()
{
// ... do your work
for (var i = 0; i < 100; i++)
{
EnqueueEmail(new EmailModel {
To = $"recipient{i}@foo.com",
Message = $"Message {i}" });
}
}
// i.e. Queue work for the email sender
private void EnqueueEmail(EmailModel message)
{
_enqueuer.Add(message);
}
}
public class EmailSender
{
private readonly BlockingCollection<EmailModel> _mailQueue;
private readonly TaskCompletionSource<string> _tcsIsCompleted
= new TaskCompletionSource<string>();
public EmailSender(BlockingCollection<EmailModel> mailQueue)
{
_mailQueue = mailQueue;
}
public void Start()
{
Task.Run(() =>
{
try
{
while (!_mailQueue.IsCompleted)
{
var nextMessage = _mailQueue.Take();
SendEmail(nextMessage).Wait();
}
_tcsIsCompleted.SetResult("ok");
}
catch (Exception)
{
_tcsIsCompleted.SetResult("fail");
}
});
}
public async Task Stop()
{
_mailQueue.CompleteAdding();
await _tcsIsCompleted.Task;
}
private async Task SendEmail(EmailModel message)
{
// IO bound work to the email server goes here ...
}
}
Example of bootstrapping and starting the above producer / consumer classes:
public static async Task Main(string[] args)
{
var theQueue = new BlockingCollection<EmailModel>(new ConcurrentQueue<EmailModel>());
var primaryWork = new PrimaryWork(theQueue);
var mailSender = new EmailSender(theQueue);
mailSender.Start();
primaryWork.DoWork();
// Wait for all mails to be sent
await mailSender.Stop();
}
I've put a complete sample up on Bitbucket here
Other Notes
- The blocking collection (and the backing
ConcurrentQueue
) are thread safe, so you can concurrently use more than one many producer and consumer thread.
- As per above, batching is encouraged, and asynchronous parallelism is possible (Since each mail sender uses a thread,
Task.WaitAll(tasks)
will wait for a batch of tasks). A totally asynchronous sender could obviously use await Task.WhenAll(tasks)
.
- As per comments below, I believe the nature of your system (i.e. Windows Service, with 2k messages / minute) warrants at least one dedicated thread for email sending, despite emailing likely being inherently IO bound.