保持单个后台线程通过Parallel.ForEach

时间:2017-07-18 22:04:29

标签: c# multithreading

(好吧,我已经弄得一团糟了,所以我打算清理它并提供我在这里可以提供的所有信息。请查看本文的底部以获得更好的解释我想要做的事情。我将其余的留在这里,希望有人能看到我制造的混乱,并学习如何从我的错误中找到更好的帖子。:))

请参阅标题为"更好的解释"因为,更好的解释。

编辑2 :我很抱歉不清楚。 ItemStore在这种情况下不是集合,它是由DB支持的服务。我已经更新了我的代码。

编辑:其他信息。

  • 后备存储将成为数据库。这意味着我们可以将项目保留在队列中,而不必担心在应用程序死亡时丢失项目。这也意味着从DB添加/检索项目可能会变慢。 (即,没有内存中那么快)。

  • 正因为如此,这也意味着我们不想一直在内存中保存一个集合。首选项是返回DB以获取下一个项目,再次为持久性安全性。

  • 最终,商品来自网络服务电话。从本质上讲,Enqueue将是一个WebAPI路由,浏览器将对其进行HTTP POST。

  • 最后,我们试图解决的核心问题是将潜在的一堆请求集中到单个FIFO队列中,这主要是由于我们的第三方库的限制使用。因此,目标是获得10个同步请求,并逐个处理它们。

我不知道这些额外信息有多少帮助,但确实如此。 :)

我试图创建一个简单的处理队列。在Enqueue上,它将一个项添加到商店,然后检查处理线程是否已经运行。如果是,那就完成了;如果没有,它将启动运行队列的线程。

队列线程本身在商店中查询其下一个项目并对其进行处理。然后它查询商店的下一个项目,并继续前进,直到它用完了项目。然后它停止处理并关闭,直到下一个项目入队。

代码基本上如下所示:

// Service that saves and retrieves items from a database
private IItemService _service;

// Item processor
private IItemProcessor _itemProcessor;

public void Enqueue(object item)
{
   _service.Save(item);
   if (_isRunning)
   {
      // If the queue is processing, the item just gets added to the DB and
      // the processing function will pull it off of the DB when needed.
      return;
   }

   // If it's not already running, process whatever queue items are in the DB.
   ProcessQueue();
}

private void ProcessQueue()
{
   _workerThread = new ThreadStart(ProcessQueueInternal);
   _workerThread.Start();
}

private void ProcessQueueInternal()
{
   // _service.GetNextItem() retrieves an item from the DB based on several
   // factors, including whether another instance of a queue has claimed it,
   // priority, etc.
   object item;
   while (item = _service.GetNextItem()) != null)
   {
      _itemProcessor.ProcessItem(item);
   }

   // No more items in the DB, so the queue should sit idle until a new
   // item is enqueued.
   _isRunning = false;
}

我正在使用Parallel.ForEach()循环测试此队列,如下所示:

Parallel.ForEach(myItems, item => Enqueue(item));

我遇到的问题是,队列偶尔会发射两次,我想避免这种情况。不经常,但它足以让我想防止这种情况发生。

我该如何解决这个问题?多个项目可能会同时排队,我需要确保一次只运行一个后台线程。最简单的方法就是这样吗?

private void ProcessQueue()
{
   if (_workerThread == null || (_workerThread != null && _workerThread.IsAlive))
   {
      _workerThread = new ThreadStart(ProcessQueueInternal);
      _workerThread.Start();
   }
}

还是有更好的方法吗?简单是这里的目标,仅次于有效性。

更好的解释

目标:汇集一堆HTTP请求,这些请求可以以这种方式触发长时间运行的进程,以便进程在单个线程上运行。工作流程如下:

  1. 用户发送HTTP POST(来自我们正在开发的Angular站点),其中包含我们执行它所需的所有信息,包括要执行的路径。

  2. POST点击了一个WebAPI ApiController,它本身就是一个服务。

    • 服务本身通过Unity实例化ContainerControlledLifetimeManager,因此它会在后台不断运行。 (我们已经测试过这种情况。)
  3. 该服务通过EntityFramework将POST中的数据添加到数据库表中。

    • 如果服务已在处理项目,则只需在此停止。
    • 如果服务没有处理项目,它就会开始这样做。
  4. 该服务通过从数据库中一次一个地检索项目来处理项目。通过获取所有数据并将HTTP POST发送到另一个服务来处理每个项目(这将启动一个无法同时运行的进程,这是我们正在使用的库的限制),然后等待它完成。一旦完成,它会将该项目的状态设置为Success / Error,然后从DB获取下一个项目并重复,直到DB中没有其他项目要处理。

  5. 根据优先级从DB中选择项目,是否已经运行(即状态为InQueue而非成功/错误)。

  6. 我们的目标有三个好处:

    1. 以数据库作为后备存储,我们对某些应用程序因某种原因而死的情况有一定的安全性。

    2. 当队列用完要处理的项目时,队列不需要继续轮询数据库。它只是闲置在那里,直到一个新项目入队,在这种情况下整个过程再次启动。

    3. 由于内部没有后备收集,因此当项目从数据库中取出并且应用程序因某种原因而死亡时,我们不必担心数据丢失。这与#1相关。

    4. 我们遇到的最大危险 - 以及我遇到的问题 - 是这里的最终切入点是网站,以及该网站上的按钮。因此,完全有可能100个人同时按下按钮,最终这个混乱的过程必须以连续的方式运行。因此,我们需要将所有这些请求汇集到单个文件行。因此,整个队列应由一个线程处理。在这里,我使用名为_workerThread的单个线程。我遇到的问题是确保_workerThread实例化并在任何周期启动一次。那就是:

      • 正在处理队列并且有新项目进入:不启动新主题
      • 队列未被处理且新项目进入:开始新线程

      我可以想到在这里模拟多个用户的唯一方法是通过Parallel.ForEach。我将在下面解释我的测试方法。

      代码:队列服务的更新代码如上所示。具体而言,EnqueueProcessQueueProcessQueueInternal是导致我出现问题的相关部分。我已将它们更新为尽可能清晰。最终,它们包含两个主要部分:

      • _service是一个单独的项目服务,负责简单的保存,删除和更新方法,以及从队列中提取下一个项目。它通过依赖注入插入队列。

      • _itemProcessor是一个负责处理项目的单独类。在现实世界中,它将创建HttpClient并在项目数据中触发请求。我把它拆开了,所以我可以创建一个假的,用于在没有数据库的情况下对队列进行单元测试。

      测试:我试图通过单元测试对此进行测试,因为我们还没有在现实世界中测试这一点所需的UI挂钩。要做到这一点,我已经制造了"假的"项目服务和项目处理器的版本:

      • 虚假物品服务只是将新队列项目存储在List<WebRequestQueueItem>中。这可能是我问题的原因,现在我想到了,但我不确定。我有点害怕为假服务使用某种线程安全的集合会添加一个&#34;修复&#34;对于现实问题(因为当队列实际在单元测试之外使用时,它将使用DB作为其后备存储)。

      • 虚假物品处理器仅执行Thread.Sleep 1500毫秒。它可以模拟正在采取的最终行动需要一段时间。

      为了模拟多个人同时点击服务器,我使用Parallel.ForEach()。我不知道更好的模拟方法。

      问题:最终问题是Parallel.ForEach()循环将所有项目一次性添加到项目服务中,但它的速度足够快,以至于队列没有&# 39;没时间意识到物品已经被处理了。所以它从另一个_workerThread开始,这正是我不希望它做的。

      我怀疑它是一般的过程,而不是我在这种情况下使用List作为后备存储的事实。不知何故,我需要确保如果项目被非常快速地添加,或者如果有几十个人一次将项目添加到队列中,则队列的多个实例不会被启动。我发现一旦队列开始,一切正常 - 可以添加新项目,并且当服务到达时它们就会得到处理。但它最初的开始导致了我的问题。

      关于数据库服务本身的说明:它使用EntityFramework,以及添加/更新/删除项目的标准方法。我们的整个产品的模式是相同的,我们还没有遇到任何我不知道的问题。不过,这些方法看起来像这样:

      添加

      _context.WebRequestQueueItems.Add(someItemEntity);
      _context.SaveChanges();
      

      更新

      _context.WebRequestQueueItems.AddOrUpdate(someItemEntity);
      _context.SaveChanges();
      

      删除

      _context.WebRequestQueueItems.Remove(someItemEntity);
      _context.SaveChanges();
      

      GetNextItem(粗略;条款稍微复杂一点,但你明白了)

      return _context
             .WebRequestQueueItems
             .OrderByDescending(item => item.Priority)
             .FirstOrDefault();
      

1 个答案:

答案 0 :(得分:1)

对于初学者,请尝试以下代码:

private static object _gate = new object();

private void ProcessQueue()
{
    if (_workerThread == null || (_workerThread != null && _workerThread.IsAlive))
    {
        lock (_gate)
        {
            if (_workerThread == null || (_workerThread != null && _workerThread.IsAlive))
            {
                _workerThread = new ThreadStart(ProcessQueueInternal);
                _workerThread.Start();
            }
        }
    }
}

此代码将阻止两个线程同时启动,但它不会阻止线程在第一个if之后但在第二个之前空闲的情况。您必须确保在多个地方呼叫ProcessQueue以确保您的队列不会停止。