-1

As I am working on processing bulk of emails, I have used the Task method to process those emails asynchronously without affecting the primary work. (Basically sending email functionality should work on different thread than primary thread.) Imagine you're processing more than 1K email per 30 seconds in Windows Service.

The issue I am facing is-- many time the Task method is not executed, it completely behave randomly. Technically, it schedules the task randomly. Sometime I receive the call in SendEmail method and sometime not. I have tried both the approaches as mentioned below.

Method 1

public void ProcessMails()
{
    Task.Run(() => SendEmail(emailModel));
}

Method 2

public async void ProcessMails()
{
    // here the SendEmail method is awaitable, but I have not used 'await' because 
    // I need non-blocking operation on main thread.
    SendEmail(emailModel));
}

Would anybody please let me know what could be the problem OR I am missing anything here?

StuartLC
  • 104,537
  • 17
  • 209
  • 285
nunu
  • 3,184
  • 10
  • 43
  • 58
  • How are you being certain that the call to *SendEmail* is being received or not? – JayV Jun 14 '18 at 11:54
  • Do you have other code in `Method 2` than this 1 call?? – Mike Jun 14 '18 at 11:55
  • @JayV yes, I am 100% sure that call to SendEmail occurs random basis. If I remove the Task.Run() and just call the SendEmail method as a normal call, it works 100% all time. – nunu Jun 14 '18 at 11:56
  • @Mike No, this is the only method call I have in _ProcessMail_ method, also there is no other method call inside _SendEmail_ method too.. – nunu Jun 14 '18 at 11:58
  • 4
    `Task.Run` isn't behaving randomly, it is likely that you are creating way too many `Tasks`, each of which needs to be scheduled to a thread, and the delay you are experiencing is likely due to thread starvation. Instead of unawaited Tasks or `Task.Run`, why not have a dedicated background thread Sending emails, which can be asynchronously queued by your `primary work`. [`BlockingCollection`](https://learn.microsoft.com/en-us/dotnet/standard/collections/thread-safe/blockingcollection-overview) for instance. – StuartLC Jun 14 '18 at 11:58
  • @StuartLC I think, your assumptions are correct, I am creating way too many tasks, internally too many threads. Thanks for the suggestions for blocking collection. – nunu Jun 14 '18 at 12:01
  • Does your email library support `async` methods, something like `SendEmailAsync(...)`. Right now niether method is async, the first is just fire-forget and the second is almost identical to synchronous method. In regard to method 2, marking the method with `async` and yet not using `await` key word is useless here. Finally there's no reason to for that method to be `async void`, even it was actually async you would be able to to know it's completion/fault. – JSteward Jun 14 '18 at 15:25
  • Just read the comment in your code: _I need non-blocking operation on main thread_, understand that `await` is not a blocking call. – JSteward Jun 14 '18 at 15:29

2 Answers2

0

As has been noted it seems as though you're running out resources to schedule tasks that ultimately send your emails. Right now the sample code tries to force feed all the work that needs to get scheduled immediatly.

The other answer provided suggects using a blocking collection but I think there is a cleaner easier way. This sample should at least give you the right idea.

using System;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;

namespace ClassLibrary1
{

    public class MailHandler
    {
        private EmailLibrary emailLibrary = new EmailLibrary();
        private ExecutionDataflowBlockOptions options = new ExecutionDataflowBlockOptions() { MaxDegreeOfParallelism = Environment.ProcessorCount };
        private ActionBlock<string> messageHandler;

        public MailHandler() => messageHandler = new ActionBlock<string>(msg => DelegateSendEmail(msg), options);

        public Task ProcessMail(string message) => messageHandler.SendAsync(message);

        private Task DelegateSendEmail(string message) => emailLibrary.SendEmail(message);
    }

    public class EmailLibrary
    {
        public Task SendEmail(string message) => Task.Delay(1000);
    }
}
JSteward
  • 6,833
  • 2
  • 21
  • 30
  • `DelegateSendEmail` shouldn't be public if you're using an approach like this. It's also unlikely that the correct degrees of parallelism is going to be the processor count, given that the operation is IO bound and not CPU bound. – Servy Jun 14 '18 at 15:45
  • 1
    Yes `DelegateSendEmail` doesn't need to be and shouldn't be public. But what would be a good default MDOP for an IO bound operation, typically I'll use unbound in the absence of other constraints? – JSteward Jun 14 '18 at 15:49
-1

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.
StuartLC
  • 104,537
  • 17
  • 209
  • 285
  • Creating a new thread and spending all of the effort you've created just to schedule work for it all so that it can synchronously block on an asynchronous operation is actively counter productive. You are not accomplishing anything just creating a thread to do nothing but be scheduled things to wait on, without actually doing anything productive. – Servy Jun 14 '18 at 13:11
  • Hence the comments on the background thread, about long lived connections to whatever IO bound mail service, plus optimizations that a batching process could do, like batch, or group by recipient, etc. Neither of these are possible in the OP's original designs of one mail, one send. – StuartLC Jun 14 '18 at 13:21
  • Neither of those optimizations require creating a thread just to sit there and do nothing while work is done asynchronously. You can still batch operations together without blocking a thread pool thread designed specifically for short operations indefinitely, or have a single open connection that is shared, again, without blocking a thread pool thread. – Servy Jun 14 '18 at 13:24
  • OP's indicated this is a Windows Service, presumably dedicated for this one purpose. This isn't asp.net. Why the obsession with saving the background thread? – StuartLC Jun 14 '18 at 13:24
  • What's the obsession with creating a thread just to do nothing productive at all? – Servy Jun 14 '18 at 13:29
  • This saves the proliferation of Tasks (and threads) that the OP's original issue has, by enqueing work to allow load to be evenly distributed. This approach has worked for 30 years. Saving a single thread on a Windows Service seems obsessive. I'm guessing you're more for a [TPL DataFlow](https://stackoverflow.com/q/21225361/314291) approach? – StuartLC Jun 14 '18 at 13:33
  • Sure, having one thread sitting around doing nothing useful is less bad than having dozens of threads sitting around doing nothing useful. By why do *either* when you could just have *no* threads sitting around doing nothing useful? You even have the benefit of not spending all of the time doing all of the scheduling that is adding nothing productive, which means both less code that needs to run, and less code you need to maintain. – Servy Jun 14 '18 at 13:37
  • And then there's the fact that this solution serializes all of the operations for no particular benefit (there was no requirement that the operations be serialized, rather than running in parallel), and given the load being talked about, it's possible that it won't even be fast enough to keep up. – Servy Jun 14 '18 at 13:39
  • OP's currrent IO load looks of the order of 2k sends / minute, or > 30 / second. I'm still asserting that this warrants sufficient attention to warrant a dedicated thread. – StuartLC Jun 14 '18 at 19:14
  • The more sends going on the more important it is to *not* go around creating threads that do nothing but wait for other things to happen. Again, your thread *is doing nothing useful*. All it's doing is adding overhead. – Servy Jun 14 '18 at 19:37
  • 1
    Who cares about a few threads? My box here has 1014 threads ATM, most doing nothing but waiting for I/O or inter-thread comms. It's nothing. – Martin James Jun 15 '18 at 09:38