限制电子邮件发送速度的过程

6

抱歉标题有点糟糕,我无法准确表达。

编辑:我应该指出这是一个控制台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()时递增,并在发送完成后递减。在初始循环中调用SendAnEmail()之前,检查计数器,如果太高,则休眠一小段时间,然后再次检查。
我不确定这是个好主意,希望SO的人们能够给出解决方法。
我对线程的了解很少,不确定在这里是否可以使用。例如,在后台线程中发送电子邮件,首先检查子线程的数量,以确保没有使用太多。或者是否有内置的“线程限制”类型。
更新
根据Steven A. Lowe的建议,我现在有:
- 一个包含我的电子邮件和唯一键的字典(这是电子邮件队列) - 一个填充队列的方法 - 一个处理队列的方法,它是一个后台线程。它检查队列,并异步发送任何在队列中的电子邮件。 - 一个SendCompleted委托,它从队列中删除电子邮件。然后再次调用FillQue。
我对这个设置有一些问题。我认为我错过了后台线程所需要的,我应该为字典中的每个项目生成一个后台线程吗?如果电子邮件队列为空,如何让线程“挂起”,不继续执行?
最终更新
我在后台线程中放了一个“while(true){}”。如果队列为空,它会等待几秒钟并重试。如果队列反复为空,我会“break”这个while,程序就会结束...工作得很好。但是我有点担心“while(true)”这个东西...

将 while(true) 改为 while(keepWorking),当您想要中断/停止线程时,将 keepWorking 设置为 false。 - Steven A. Lowe
有最终解决方案和源代码示例吗? - Kiquenet
5个回答

2

简短回答

使用队列作为有限缓冲区,由其自身的线程处理。

详细解释

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

你只需要在队列操作周围加锁,并且只需直接管理一个线程以处理队列。你永远不会有超过N+1个活动线程,其中N是队列限制。


这听起来像是。您能否用简短的代码示例详细说明一下这个答案?对于队列使用什么类型最理想(IList?)? - ChadT
@[DaRKoN_]: 是的,我可能会使用IList<item>或Queue<item>来代替你在原始问题中使用的“collection”中的“item”,即你需要创建和发送电子邮件的任何信息 - 自定义类、数据行等。 - Steven A. Lowe

1

我相信你的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 ...
  }
}

1
我会将所有消息添加到队列中,然后生成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
        }
    }
}

如果消息相同,只是有多个接收者,请将消息与接收者交换,并向发送方法添加单个消息参数。

即使使用信号量来限制n个活动工作线程,你也不想产生10000个任务然后等待。由于每个工作线程运行时间较长,因此“专用线程池”是有意义的选择。 - Richard
啥?生成10000个任务?你在说啥呢? - Svish

0
你可以使用 .NET 定时器设置发送消息的计划。每当定时器触发时,获取下一个10条消息并全部发送,然后重复此过程。或者如果您想要一个普遍的(每秒10条消息)速率,可以将定时器设置为每100毫秒触发一次,并每次发送单个消息。
如果需要更高级的调度功能,可以考虑使用类似 Quartz.NET 的调度框架。

感谢您的回复,这种方法的另一个副作用是我可以轻松地限制从数据库中检索的结果数量,每次仅获取10个。 - ChadT

0

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

你的想法是正确的,后台线程可以在这里发挥很好的作用。基本上,你想要做的就是为这个进程创建一个后台线程,让它按照自己的方式运行,包括延迟等待,然后在进程完成时终止线程,或者无限期地保留它(将其转换为Windows服务或类似的东西是一个好主意)。

可以在这里阅读一些关于多线程的介绍(包括Thread.Sleep!)

可以在这里阅读一个关于Windows服务的介绍


未找到http://www.codeproject.com/KB/dotnet/simplewindowsservice.aspx相关内容。 - Kiquenet

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接