我应该在服务器上使用Parallel.ForEach来同时发出许多Web请求

时间:2014-11-19 23:51:39

标签: c# multithreading parallel-processing threadpool parallel.foreach

我已经阅读了很多关于Parallel.ForEach的内容,但我的问题并没有真正找到答案。

我们有一个Windows服务,每隔几分钟从多个数据库中提取行,并使用foreach循环,通过Web请求发送这些行以完成操作。因此,所有这些Web请求目前都按顺序完成,耗时太长,因此我们希望并行运行它们。

我的初步调查让我相信Producer-Consumer approach using threads是最好的,生成器每隔几分钟就会将行放入一个线程安全的队列中,并且在服务初始化期间,我只需启动一些消费者线程(例如10个,但可能是100个或更多),它不断检查队列以查看是否有需要通过Web请求发送的行。

一位同事建议只需将foreach循环更改为Parallel.ForEach。我对此的第一个担心是,ForEach会阻止所有操作,直到枚举中的所有项目都完成,所以如果它有10个项目,9个在5秒内完成,一个在5分钟内完成,那么它基本上什么都不做,只有一个请求持续4分55秒只需在新线程中执行Parallel.ForEach即可克服这一点,如下所示:

Task.Factory.StartNew( () => Parallel.ForEach<Item>(items, item => DoSomething(item)));

所以有了这个,每隔几分钟就会发生一个新的Parallel.ForEach循环,它将自上次检查后添加到数据库中的所有新行启动,即使之前的Parallel.ForEach循环没有已完成(即5分钟长的请求不会阻止新请求被发送)。

这很简单,可以最大限度地减少需要大幅度进行的代码更改,但我仍然担心在托管其他服务和网站的服务器上运行此代码。我已经读过Parallel.ForEach可能会固定服务器上的所有CPU,即使简单的Web请求不是CPU密集型操作。我知道我可以通过使用MaxDegreeOfParallelism property来限制循环将使用的线程数,因此我将其设置为10或100或其他。这很好,因为没有10或100个任务不断运行而且什么都不做,Parallel.ForEach只会旋转多少它需要然后在循环完成时关闭它们。但我仍然犹豫是否可能在服务器上消耗太多资源。

那么这些选项(或其他)中的哪一个最适合我的场景?我对在服务器计算机上使用Parallel.ForEach的担忧是否合理?它看起来很简单&#34;更简单&#34;和&#34; lazier&#34;解决方案,所以我只是想确保如果我们继续使用它就不会再咬我了。另外,我并不关心将此解决方案扩展到多个服务器;只运行一台同时运行其他服务和网站的服务器。

更新

评论要求提供一些源代码以提供更多背景信息。

以下是我们目前正在做的简化版本:

void FunctionGetsCalledEvery2Minutes()
{
    // Synchronously loop over each database that we need to check.
    foreach (var database in databasesToCheck)
    {
        // Get the rows from this database.
        var rows = database.GetRowsFromTable();

        // Synchronously send each row to a web service to be processed.
        foreach (var request in rows)
        {
            SendRequestToWebServiceToBeProcessed(request);
        }
    }
}

SendRequestToWebServiceToBeProcessed(DatabaseRow request)
{
    // Request may take anywhere from 1 second to 10 minutes.
    Thread.Sleep(_randomNumberGenerator.Next(1000, 600000));
}

以下是使用Parallel.ForEach代码的简化版本:

void FunctionGetsCalledEvery2Minutes()
{
    // Synchronously loop over each database that we need to check.
    foreach (var database in databasesToCheck)
    {
        // Get the rows from this database.
        var rows = database.GetRowsFromTable();

        // Asynchronously send each row to a web service to be processed, processing no more than 30 at a time.
        // Call the Parallel.ForEach from a new Task so that it does not block until all rows have been sent.
        Task.Factory.StartNew(() => Parallel.ForEach<DatabaseRow>(rows, new ParallelOptions() { MaxDegreeOfParallelism = 30 }, SendRequestToWebServiceToBeProcessed));
    }
}

以下是使用producer-consumer代码的简化版本:

private System.Collections.Concurrent.BlockingCollection<DatabaseRow> _threadSafeQueue = new System.Collections.Concurrent.BlockingCollection<DatabaseRow>();
void FunctionGetsCalledEvery2Minutes()
{
    // Synchronously loop over each database that we need to check.
    foreach (var database in databasesToCheck)
    {
        // Get the rows from this database.
        var rows = database.GetRowsFromTable();

        // Add the rows to the queue to be processed by the consumer threads.
        foreach (var row in rows)
        {
            _threadSafeQueue.Add(row);
        }
    }
}

void ConsumerCode()
{
    // Take a request off the queue and send it away to be processed.
    var request = _threadSafeQueue.Take();
    SendRequestToWebServiceToBeProcessed(request);
}

void CreateConsumerThreadsOnApplicationStartup(int numberOfConsumersToCreate)
{
    // Create the number of consumer threads specified.
    for (int i = 0; i < numberOfConsumersTo; i++)
    {
        Task.Factory.StartNew(ConsumerCode);
    }
}

在这个例子中我有一个同步生成器,但是我可以轻松地为每个数据库启动一个异步生成器线程来进行轮询。

这里需要注意的一点是,在Parallel.ForEach示例中,我将其限制为一次最多只处理30个线程,但这仅适用于该一个实例。如果2分钟过去并且Parallel.ForEach循环仍然有10个请求尚未完成,它将启动30个新线程,总共40个线程同时运行。因此,如果Web请求的超时时间为10分钟,我们可能很容易遇到同时运行150个线程的情况(10分钟/ 2分钟=函数调用5次*每个实例30个线程= 150)。这是一个潜在的问题,好像我提高了最大线程允许的数量,或者开始以比2分钟更短的时间间隔调用该函数,我可能很快就会同时运行数千个线程,在服务器上消耗的资源比我多想。这是一个有效的问题吗?消费者 - 生产者方法没有这个问题;它只会运行我为numberOfConsumersToCreate变量指定的线程数。

有人提到我应该使用TPL Dataflows,但我以前从未使用它们,也不想花费大量时间在这个项目上。如果TPL Dataflows仍然是我想知道的最佳选择,但我也想知道这两种方法中的哪一种(Parallel.ForEach vs. Producer-Consumer)对我的方案更好。

希望这会提供更多背景信息,以便我可以获得更好的目标答案。谢谢:))

2 个答案:

答案 0 :(得分:1)

如果您有许多短操作和偶尔长时间操作,Parallel.ForEach将阻塞,直到所有操作完成。然而,虽然它正在处理这一个长期请求,但它不会挂起所有核心,只是那个仍在工作的核心。请记住,当有许多项目正在处理时,它将尝试使用所有核心。

编辑:

使用MaxDegreeOfParallelism属性时,没有理由将其设置为CPU可以运行的线程数(超过内核数和超线程程度)。实际上,将它减少到低于该值的数字是唯一有用的。

由于阻止不是问题Parallel.ForEach,如果你的项目真的可以同时运行,那么看似懒惰是非常合适的。

答案 1 :(得分:-1)

我对你的代码并不深入,但我有一些经验/建议。

并行代码确实可以很快,它取决于在四核上启动数百个线程的内核数量并不理想,如果有4个线程(通常)会更好,我知道有一些情况。但一般来说,你不需要考虑它,因为最新的.net版本处理它。

然而,并行代码还有另一个重要问题 你无法控制事情的执行顺序。 所以,如果你做一个console.print(i),其中我就像下一个参数,从0到100,然后在屏幕上你将看不到1,2,3,4,5,6,7 但有些混乱,因为每个线程打印出他的部分数字范围,你会看到像1,14,37,70,2,15,80,......每个数字都写一次,但是它们的顺序是不合逻辑的。

如果您有一些复杂的数据库数学,请记住最后一件事 您需要组合多个查找执行复杂的计算,然后创建一个新表。然后,如果“复杂计算”可以并行执行,您可能会发现速度提升。如果复杂计算可以为您的数据库创建一个具有唯一键的新键值对,那么你可以。

但是parralel数学可能存在问题。 你需要更新一个值,但是另一个'线程'也需要更新它,那么最终结果是什么?,我知道有等待和锁定机制,但是如果你在这种情况下我发现它通常是最好的重新考虑重新设计代码,并重新考虑你的问题。

也许创建额外数组的列表字典或表,暂时存储结果然后将它们组合起来,这是我解决这些问题的常用方法。

尝试编写这样的数学/逻辑短路,并且简单如此处所述,您通常可以实现极佳的速度提升。我知道可以做更多的事情,但保持逻辑简单将会很快,如果可能尝试坚持简单的逻辑,因为它也保持你的代码干净。一个人使用parralel代码的地方已经足够复杂了。

还有一点需要注意,如果您的复杂计算可能会导致执行时间变化(如果需要检查一些,可能会增加额外的复杂性)。然后,与一些巨大的复杂代码部分相比,通常可能更好地启动大量的迷你“数学/代码”部分。具有迷你任务的巨大线程队列作为具有大量任务的小队列更快完成。