【问题标题】:Throttling speed of email sending process电子邮件发送过程的限制速度
【发布时间】:2009-03-18 04:34:46
【问题描述】:

对不起,标题有点蹩脚,我说得不好。

编辑:我应该注意这是一个控制台 c# 应用程序

我已经制作了一个这样工作的系统原型(这是粗略的伪代码):

var collection = grabfromdb();

foreach (item in collection) {
    SendAnEmail();
}

发送电子邮件:

SmtpClient mailClient = new SmtpClient;
mailClient.SendCompleted += new SendCompletedEventHandler(SendComplete);
mailClient.SendAsync('the mail message');

发送完成:

if (anyErrors) {
    errorHandling()
}
else {
    HitDBAndMarkAsSendOK();    
}

显然这种设置并不理想。如果最初的集合有 10,000 条记录,那么它将以相当短的顺序新建 10,000 个 smtpclient 实例,并尽可能快地遍历行 - 并且可能会在此过程中加速。

我理想的最终结果是同时发送 10 封电子邮件。

想到了一个 hacky 解决方案:添加一个计数器,当 SendAnEmail() 被调用时它会增加,当 SendComplete 被发送时它会减少。在初始循环调用 SendAnEmail() 之前,检查计数器,如果它太高,则休眠一小段时间,然后再次检查。

我不确定这是否是个好主意,并且认为 SO 蜂巢思维有办法正确地做到这一点。

我对线程知之甚少,不确定它是否适合在这里使用。例如,在后台线程中发送电子邮件,首先检查子线程的数量以确保没有使用太多。或者,如果内置某种类型的“线程节流”。


更新

按照 Steven A. Lowe 的建议,我现在有:

  • 包含我的电子邮件和唯一键的字典(这是电子邮件队列
  • 填充字典的 FillQue 方法
  • ProcessQue 方法,它是一个后台线程。它检查队列,并发送队列中的任何电子邮件。
  • 从队列中删除电子邮件的 SendCompleted 委托。并再次调用 FillQue。

我在使用此设置时遇到了一些问题。我想我错过了背景线程的船,我应该为字典中的每个项目生成其中一个吗?如果电子邮件队列清空线程结束,我怎样才能让线程“闲逛”,因为缺少更好的词。


最终更新

我在后台线程中放置了一个“while(true) {}”。如果 que 是空的,它会等待几秒钟,然后再试一次。如果 que 反复为空,我会在此期间“中断”,然后程序结束......工作正常。不过,我有点担心'while(true)' 业务..

【问题讨论】:

  • 将while(true)改为while(keepWorking),当你想中断/停止线程时将keepWorking设置为false
  • 任何最终解决方案和源代码示例?

标签: c# multithreading smtpclient


【解决方案1】:

简答

将队列用作有限缓冲区,由其自己的线程处理。

长答案

调用一个填充队列方法来创建一个电子邮件队列,限制为(比如说)10 封。用前 10 封未发送的电子邮件填充它。启动一个线程来处理队列 - 对于队列中的每封电子邮件,将其异步发送。当队列为空时,睡眠一段时间并再次检查。让完成委托从队列中删除已发送或出错的电子邮件并更新数据库,然后调用 fill-queue 方法将更多未发送的电子邮件读取到队列中(备份到限制)。

您只需要锁定队列操作,并且只需要(直接)管理一个线程来处理队列。您一次不会有超过 N+1 个线程处于活动状态,其中 N 是队列限制。

【讨论】:

  • 这听起来像。你能用一个简短的代码示例来充实这个答案吗?用于 que 的理想类型是什么(IList?)?
  • @[DaRKoN_]:是的,我可能会使用 IList 或 Queue 来表示您在原始问题的“集合”中使用的任何“项目”,即任何信息您需要创建和发送电子邮件 - 自定义类、数据行等。
【解决方案2】:

我相信您的 hacky 解决方案实际上会奏效。只需确保您在递增和递减计数器的位周围有一个锁定语句:

class EmailSender
{
  object SimultaneousEmailsLock;
  int SimultaneousEmails;
  public string[] Recipients;

  void SendAll()
  {
    foreach(string Recipient in Recipients)
    {
      while (SimultaneousEmails>10) Thread.Sleep(10);
      SendAnEmail(Recipient);
    }
  }

  void SendAnEmail(string Recipient)
  {
    lock(SimultaneousEmailsLock)
    {
      SimultaneousEmails++;
    }

    ... send it ...
  }

  void FinishedEmailCallback()
  {
    lock(SimultaneousEmailsLock)
    {
      SimultaneousEmails--;
    }

    ... etc ...
  }
}

【讨论】:

    【解决方案3】:

    我会将我所有的消息添加到队列中,然后生成 10 个线程,这些线程发送电子邮件直到队列为空。伪 C#(可能无法编译):

    class EmailSender
    {
        Queue<Message> messages;
        List<Thread> threads;
    
        public Send(IEnumerable<Message> messages, int threads)
        {
            this.messages = new Queue<Message>(messages);
            this.threads = new List<Thread>();
            while(threads-- > 0)
                threads.Add(new Thread(SendMessages));
    
            threads.ForEach(t => t.Start());
    
            while(threads.Any(t => t.IsAlive))
                Thread.Sleep(50);
        }
    
        private SendMessages()
        {
            while(true)
            {
                Message m;
                lock(messages)
                {
                    try
                    {
                        m = messages.Dequeue();
                    }
                    catch(InvalidOperationException)
                    {
                        // No more messages
                        return;
                    }
                }
    
                // Send message in some way. Not in an async way, 
                // since we are already kind of async.
    
                Thread.Sleep(); // Perhaps take a quick rest
            }
        }
    }
    

    如果消息相同,并且只有多个收件人,只需将消息与收件人交换,并将单个消息参数添加到 Send 方法。

    【讨论】:

    • 即使使用信号量来限制 n 个活跃的工作人员,您也不希望产生 10000 个任务然后等待。这与每个工人长时间(ish)运行意味着“专用线程池”是有意义的。
    • 嗯?产生10000个任务?你在说什么?
    【解决方案4】:

    您可以使用 .NET 计时器来设置发送消息的时间表。每当计时器触发时,抓取接下来的 10 条消息并将它们全部发送,然后重复。或者,如果您想要一个一般的(每秒 10 条消息)速率,您可以让计时器每 100 毫秒触发一次,并且每次发送一条消息。

    如果您需要更高级的调度,可以查看类似Quartz.NET 的调度框架

    【讨论】:

    • 感谢您的回复,这种方法的另一个副作用是我可以轻松地将数据库拉回的结果数量限制为一次只能抓取 10 个。
    【解决方案5】:

    这不是Thread.Sleep()可以处理的吗?

    您认为后台线程在这里可以起到很好的作用是正确的。基本上你想要做的是为这个进程创建一个后台线程,让它按照自己的方式运行,延迟等等,然后在进程完成时终止线程,或者无限期地保持它开启(把它变成一个 Windows 服务或类似的东西会是个好主意)。

    一点点intro on multi-threading can be read here (with Thread.Sleep included!)

    一个不错的intro on Windows Services can be read here

    猜你喜欢
    • 2017-09-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-17
    • 2011-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多