【问题标题】:Partial work being done twice (ThreadPool.QueueUserWorkItem)部分工作被完成两次 (ThreadPool.QueueUserWorkItem)
【发布时间】:2012-03-26 11:35:30
【问题描述】:

我创建了一个通讯系统,允许我指定哪些成员应该接收通讯。然后,我遍历符合条件的成员列表,并为每个成员生成个性化消息并异步向他们发送电子邮件。

当我发送电子邮件时,我使用的是ThreadPool.QueueUserWorkItem

由于某种原因,一部分成员收到了两次电子邮件。在我的最后一批中,我只发送了 712 个成员,但最终发送了 798 条消息。

我正在记录发送出去的消息,我可以看出前 86 位成员收到了两次消息。这是日志(按照发送消息的顺序)

No.  Member   Date
1.   163992   3/8/2012 12:28:13 PM
2.   163993   3/8/2012 12:28:13 PM
...
85.   164469   3/8/2012 12:28:37 PM
86.   163992   3/8/2012 12:28:44 PM
87.   163993   3/8/2012 12:28:44 PM
...
798.   167691   3/8/2012 12:32:36 PM

但是,每个成员都应该收到一次时事通讯,您可以看到成员 163992 收到消息 #1 和 #86;成员163993收到消息#2和#87;等等。

另外需要注意的是,发送消息 #85 和 #86 之间有 7 秒的延迟。

我已经多次查看代码并排除了几乎所有代码都是导致它的原因,除了可能是ThreadPool.QueueUserWorkItem

这是我第一次使用 ThreadPool,所以我对它不是很熟悉。是否可能存在导致这种行为的某种竞争条件?

=== --- 代码示例 --- ===

    foreach (var recipient in recipientsToEmail)
    {
        _emailSender.SendMemberRegistrationActivationReminder(eventArgs.Newsletter, eventArgs.RecipientNotificationInfo, previewEmail: string.Empty);
    }


    public void SendMemberRegistrationActivationReminder(DomainObjects.Newsletters.Newsletter newsletter, DomainObjects.Members.MemberEmailNotificationInfo recipient, string previewEmail)
    {
//Build message here .....

//Send the message
            this.SendEmailAsync(fromAddress: _settings.WebmasterEmail,
                                toAddress: previewEmail.IsEmailFormat()
                                            ? previewEmail
                                            : recipientNotificationInfo.Email,
                                subject: emailSubject,
                                body: completeMessageBody,
                                memberId: previewEmail.IsEmailFormat()
                                            ? null  //if this is a preview message, do not mark it as being sent to this member
                                            : (int?)recipientNotificationInfo.RecipientMemberPhotoInfo.Id,
                                newsletterId: newsletter.Id,
                                newsletterTypeId: newsletter.NewsletterTypeId,
                                utmCampaign: utmCampaign,
                                languageCode: recipientNotificationInfo.LanguageCode);
        }

    private void SendEmailAsync(string fromAddress, string toAddress, string subject, MultiPartMessageBody body, int? memberId, string utmCampaign, string languageCode, int? newsletterId = null, DomainObjects.Newsletters.NewsletterTypeEnum? newsletterTypeId = null)
    {
        var urlHelper = UrlHelper();
        var viewOnlineUrlFormat = urlHelper.RouteUrl("UtilityEmailRead", new { msgid = "msgid", hash = "hash" });
        ThreadPool.QueueUserWorkItem(state => SendEmail(fromAddress, toAddress, subject, body, memberId, newsletterId, newsletterTypeId, utmCampaign, viewOnlineUrlFormat, languageCode));
    }

【问题讨论】:

  • 对我来说看起来像竞争条件 - 如果您使用队列,您会在调用 ThreadPool.QueueUserWorkItem() 之前从队列中删除项目吗?我们可以看看你的代码吗?
  • 我没有使用任何其他类型的队列。基本上:遍历满足要求的成员列表,为成员生成电子邮件,添加对实际向 ThreadPool 发送电子邮件的方法的调用。
  • 为避免重复维护有电子邮件待处理的用户列表
  • 用给出的信息几乎不可能说出任何事情。我建议发布演示该问题的代码。
  • recipientsToEmail 是否有可能只在其中包含两次收件人 ID?我想我真正的问题是“你确定使用ThreadPool.QueueUserWorkItem 会导致重复吗?”

标签: c# asp.net threadpool


【解决方案1】:

您确定您正在运行以获取要向其发送电子邮件的成员列表的查询没有重复项吗?您要加入另一张桌子吗?你可以做的是:

List<DomainObjects.Members.MemberEmailNotificationInfo> list = GetListFromDatabase();
list = list.Distinct().ToList();

【讨论】:

    【解决方案2】:

    在您的代码示例中,我们看不到您的日志记录发生在哪里。

    可能是发送邮件的方法误认为出现问题,系统重试,可能导致邮件发送两次。

    另外,正如其他答案和评论中所写,我会再次检查收件人列表中是否有重复条目,并在非并行上下文中对其进行测试。

    【讨论】:

      【解决方案3】:

      在服务器上运行 800 多个线程并不是一个好习惯! 尽管您使用的是 ThreadPool,但线程正在服务器上排队并在旧线程返回池并释放资源时运行。这在服务器上可能需要几分钟时间,并且在此期间可能会发生许多情况,例如竞争条件或并发。 您可以改为在一个受保护列表上排队一个工作项:

      lock (recipientsToEmail)
      {
          ThreadPool.QueueUserWorkItem(t =>
              {
                  // enumerate recipientsToEmail and send email
              });
      }
      

      【讨论】:

      • 当我发送时事通讯时,我利用的是一次发送一封邮件时已经存在的电子邮件系统。并没有真正考虑过,但是是的,拥有 800 多个线程似乎确实是错误的方法。我重新编写了我的代码,以便启动一个新线程并让它处理时事通讯消息。
      【解决方案4】:

      在将任务排队到后台线程的代码中,任务执行两次的一个常见原因是错误处理错误。您可以仔细检查您的代码,以确保如果出现错误,您不会始终重试,无论错误类型如何(某些错误需要重试;其他错误则不需要)。

      话虽如此,您发布的代码并未包含足够的信息来明确回答您的问题;有很多可能性。

      FWIW,您是否知道 SmtpClient 类有一个 SendAsync() 方法,不需要使用单独的工作线程?

      【讨论】:

        【解决方案5】:

        如果这个代码:

        foreach (var recipient in recipientsToEmail)
        {
            _emailSender.SendMemberRegistrationActivationReminder(eventArgs.Newsletter
            ,eventArgs.RecipientNotificationInfo, previewEmail: string.Empty);
        }
        

        匹配你实际在做什么......你有一个明显的错误。即您正在执行 foreach 但未使用返回的值,因此您将为recipientsToEmail 中的每个条目发送相同的电子邮件至eventArgs.RecipientNotificationInfo

        【讨论】:

          【解决方案6】:

          要检查的事情(我假设您有办法模拟电子邮件的发送):

          • 重复电子邮件的数量是否总是完全相同?如果增加/减少输入值的数量会怎样?重复的用户 ID 总是相同吗?
          • SendEmail() 是否在做任何有意义的事情? (我没有看到你的代码)
          • 您是否有不使用framework's SendAsync() method 的原因?
          • 在没有多线程的情况下,您会获得相同的行为吗?

          不管怎样,从您自己的站点发送大量电子邮件(即使是完全合法的)也并不总是值得麻烦的。垃圾邮件拦截服务非常激进,您不希望您的域最终被列入黑名单。第三方服务消除了这种风险,提供了许多工具,并为您管理这部分流程。

          【讨论】:

          • Tim,实际上 SendEmail 记录了邮件的内容,以便他们可以“在线查看”。我不知道 SmtpClient 有一个 SendAsync 方法,我将切换到该方法。另外,我实际上是在使用 SendGrid 来处理电子邮件的传递。我只是在我的网站上生成邮件并使用他们的 SMTP 服务器。
          • "在没有多线程的情况下你能得到同样的行为吗?"对我来说是第一点,以及检查列表中重复的项目。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-09-26
          • 2013-09-16
          • 2018-03-12
          • 1970-01-01
          • 2012-09-09
          • 2015-05-12
          相关资源
          最近更新 更多