Task.Run()在高负载下随机/不规律地调度任务

时间:2018-06-14 11:51:39

标签: c# multithreading c#-4.0 task task-parallel-library

在处理大量电子邮件时,我使用Task方法处理这些电子邮件asynchronously,而不会影响主要工作。 (基本上发送电子邮件功能应该在与主线程不同的线程上工作。)想象一下,您在Windows Service中每30秒处理超过1K的电子邮件。

我面临的问题是 - 很多时候Task方法没有执行,它完全表现为随机。从技术上讲,它会随机安排任务。有时我会在 SendEmail 方法中收到电话,有时则不会。我尝试了下面提到的两种方法。

方法1

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

方法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));
}

有人请让我知道可能是什么问题,或者我在这里遗漏了什么?

2 个答案:

答案 0 :(得分:0)

正如已经注意到的那样,似乎您的资源不足以安排最终发送电子邮件的任务。现在,示例代码尝试强制提供所有需要立即调度的工作。

另一个答案提供了使用阻止集合的建议,但我认为有一种更简洁的方法。这个样本至少应该给你正确的想法。

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

答案 1 :(得分:-1)

鉴于发送电子邮件的频率相当高,很可能您为调度程序安排了太多Tasks

在方法1中,每次调用Task.Run都会创建一个新任务,每个任务都需要在一个线程上进行调度。通过执行此操作,您很可能正在耗尽线程池。

虽然方法2会减少任务饥饿,即使使用unawaited Task调用(fire and forget),仍然需要在Threadpool上安排完成async方法之后的继续,这将产生负面影响你的系统。

而不是unawaited TasksTask.Run,而且由于您是Windows服务,我会改为使用长期运行的后台主题来发送电子邮件。此线程可以独立于您的主要工作,并且可以通过队列将电子邮件安排到此线程。

如果单个邮件发送线程不足以跟上邮件的速度,您可以扩展EmailSender个线程的数量,但将其约束为合理的有限数量。

您还应该探索其他优化,这将再次提高您的电子邮件发件人的吞吐量,例如

  • 电子邮件发件人可以保持与邮件服务器的长期连接吗?
  • 邮件服务器是否接受批量电子邮件?

以下是使用BlockingCollection并附上电子邮件消息模型ConcurrentQueue的示例。

  • 创建一个在生产者“PrimaryWork”线程和“EmailConsumer”线程之间共享的队列(显然,如果你有一个IoC容器,最好在那里注册)
  • 在主要工作主题上输入邮件
  • 使用者EmailSender在阻塞收集队列上运行循环,直到调用CompleteAdding
  • 我使用TaskCompletionSource提供了一个任务,该任务将在所有邮件发送完毕后完成,即可以在不丢失仍在队列中的电子邮件的情况下正常退出。

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 ...
    }
}

引导和启动上述生产者/消费者类的示例:

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();
}

我已在Bitbucket here

上填写了完整的样本

其他笔记

  • 阻塞集合(以及支持ConcurrentQueue)是线程安全的,因此您可以同时使用多个生产者和消费者线程。
  • 如上所述,鼓励批处理,并且可以进行异步并行(由于每个邮件发件人使用一个线程,Task.WaitAll(tasks)将等待一批任务)。完全异步的发件人显然可以使用await Task.WhenAll(tasks)
  • 根据下面的评论,我相信你的系统的性质(即Windows服务,每分钟2k消息)保证至少有一个用于电子邮件发送的专用线程,尽管电子邮件可能具有固有的IO限制。