(好吧,我已经弄得一团糟了,所以我打算清理它并提供我在这里可以提供的所有信息。请查看本文的底部以获得更好的解释我想要做的事情。我将其余的留在这里,希望有人能看到我制造的混乱,并学习如何从我的错误中找到更好的帖子。:))
请参阅标题为"更好的解释"因为,更好的解释。
编辑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请求,这些请求可以以这种方式触发长时间运行的进程,以便进程在单个线程上运行。工作流程如下:
用户发送HTTP POST(来自我们正在开发的Angular站点),其中包含我们执行它所需的所有信息,包括要执行的路径。
POST点击了一个WebAPI ApiController
,它本身就是一个服务。
ContainerControlledLifetimeManager
,因此它会在后台不断运行。 (我们已经测试过这种情况。)该服务通过EntityFramework将POST中的数据添加到数据库表中。
该服务通过从数据库中一次一个地检索项目来处理项目。通过获取所有数据并将HTTP POST发送到另一个服务来处理每个项目(这将启动一个无法同时运行的进程,这是我们正在使用的库的限制),然后等待它完成。一旦完成,它会将该项目的状态设置为Success / Error,然后从DB获取下一个项目并重复,直到DB中没有其他项目要处理。
根据优先级从DB中选择项目,是否已经运行(即状态为InQueue而非成功/错误)。
我们的目标有三个好处:
以数据库作为后备存储,我们对某些应用程序因某种原因而死的情况有一定的安全性。
当队列用完要处理的项目时,队列不需要继续轮询数据库。它只是闲置在那里,直到一个新项目入队,在这种情况下整个过程再次启动。
由于内部没有后备收集,因此当项目从数据库中取出并且应用程序因某种原因而死亡时,我们不必担心数据丢失。这与#1相关。
我们遇到的最大危险 - 以及我遇到的问题 - 是这里的最终切入点是网站,以及该网站上的按钮。因此,完全有可能100个人同时按下按钮,最终这个混乱的过程必须以连续的方式运行。因此,我们需要将所有这些请求汇集到单个文件行。因此,整个队列应由一个线程处理。在这里,我使用名为_workerThread
的单个线程。我遇到的问题是确保_workerThread
实例化并在任何周期启动一次。那就是:
我可以想到在这里模拟多个用户的唯一方法是通过Parallel.ForEach
。我将在下面解释我的测试方法。
代码:队列服务的更新代码如上所示。具体而言,Enqueue
,ProcessQueue
和ProcessQueueInternal
是导致我出现问题的相关部分。我已将它们更新为尽可能清晰。最终,它们包含两个主要部分:
_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();
答案 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
以确保您的队列不会停止。