【问题标题】:Parallelizing a task using .AsParallel().ForAll or Parallel.ForEach performance issue使用 .AsParallel().ForAll 或 Parallel.ForEach 性能问题并行化任务
【发布时间】:2012-01-15 00:06:38
【问题描述】:

我有一个网站列表和一个代理服务器列表。

我有这个动作

Action<string> action = (string url) =>
{
    var proxy = ProxyHandler.GetProxy();
    HtmlDocument html = null;
    while (html == null)
    {
        try
        {

            html = htmlDocumentLoader.LoadDocument(url, proxy.Address);

            // Various db manipulation code

            ProxyHandler.ReleaseProxy(proxy);
        }
        catch (Exception exc)
        {
            Console.WriteLine("{0} proxies remain", ProxyHandler.ListSize());

            // Various db manipulation code

            proxy = ProxyHandler.GetProxy();
        }
    }
};

我用这个称呼

urlList.AsParallel().WithDegreeOfParallelism(12).ForAll(action);

或

Parallel.ForEach(urlList, action);

我的ProxyHandler类如下

public static class ProxyHandler
{    
    static List<Proxy> ProxyList = new ProxyRepository().GetAliveProxies().ToList();

    public static Proxy GetProxy()
    {
        lock (ProxyList)
        {
            while (ProxyList.Count == 0)
            {
                Console.WriteLine("Sleeping");
                Thread.Sleep(1000);
            }
            var proxy = ProxyList[0];
            ProxyList.RemoveAt(0);
            return proxy;
        }           
    }

    public static void ReleaseProxy(Proxy proxy)
    {
        lock (ProxyList)
        {
            if(!ProxyList.Contains(proxy))ProxyList.Add(proxy);
        }
    }

    public static int ListSize()
    {
        lock (ProxyList)
        {
            return ProxyList.Count;
        }
    }
}

我的问题是,当它执行时,它似乎真的很快完成了大约 90% 的网站,然后需要很长时间才能完成剩下的。

我的意思是,在 100 个网址中,前 90 个网址与后 10 个网址所花费的时间一样多。

我已经排除了代理已经死亡,因为没有抛出异常。似乎 urlList 上的最后一项需要很长时间才能完成。

更新:

我正在添加一些运行数据以使我的问题更清楚:

Minute    1 2   3   4   5   6   7   8   9   16  18  19
Count    23 32  32  17  6   1   1   1   1   2   1   2

正如您在前 4 分钟中看到的那样,我完成了 104/119 个请求。然后剩下的需要 15 分钟。

这看起来像是线程连接中的一个问题,但我没有发现这可能是什么。

【问题讨论】:

  • 您是否尝试过分析应用程序? Visual Studio 有一个并发分析器,可以为您提供一些线索。

标签: c# multithreading c#-4.0 task-parallel-library


【解决方案1】:

您正在浪费线程和 CPU 时间。在这种情况下,您将有 12 个线程;每个线程一次只能处理一个 url。因此,您一次只能处理 12 个 url。此外,大多数时候这些线程什么都不做(它们只是等待免费代理或加载页面),而它们可以用于更有用的任务。

为避免这种情况,您应该使用非阻塞 IO 操作。因此,您应该考虑使用其异步接口之一(htmlDocumentLoader.BeginLoadDocument/htmlDocumentLoader.EndLoadDocument 或htmlDocumentLoader.LoadDocumentAsync/htmlDocumentLoader.LoadDocumentCompleted),而不是使用htmlDocumentLoader.LoadDocument。在这种情况下,如果您有 100 个 url,所有这些都将同时加载,而不会创建额外的线程并浪费 CPU 时间。只有在页面加载完成时,才会创建新线程(实际上是从 ThreadPool 中获取)来处理它。

您等待免费代理的方式也很浪费。不要使用while (ProxyList.Count == 0) 在没有空闲代理的情况下冻结线程,而是考虑使用每秒唤醒的计时器并检查是否有空闲代理可用。这不是最好的解决方案,但至少不会浪费线程。更好的解决方案是向 ProxyHandler 添加一个事件,它会在代理可用时通知。

【讨论】:

  • while (ProxyList.Count == 0) 被放在那里以防万一,到目前为止从未如此。异步页面加载是个好主意,但我担心它会使我的解决方案过于复杂。
  • 这取决于实现。 TPL 提供了将 APM(BeginXXX、EndXXX)和 EAM(XXXAsync、XXXCompleted)模型包装到任务中的方法。然后,您可以使用 TPL 轻松操作这些任务。另外,看看 Reactive Extensions for .NET。它为处理异步操作提供了很好的工具。
【解决方案2】:

您的问题可能是由于 PLinq 使用了 Partitioner。

如果使用 Range Partitiner,您的 url 集合将分成组,每个组中的 url 数量相等。然后为每个组启动一个任务,无需进一步同步。

这意味着当所有其他任务都完成后,将有一项任务花费时间最长并且仍有工作要做。这实际上意味着操作的最后部分是单线程的。

解决方案是使用不同的分区器。您也许可以使用内置的 Chunk Partitioner,如 MSDN 中所述。

如果这还不够好,您将不得不编写/找到一个分区器实现来逐个生成元素。这是 C# 5 内置的:EnumerablePartitionerOptions

【讨论】:

  • 你说的很有道理。但这并不能解释例如在执行的最后几分钟(总运行时间为 18-25 分钟),它看起来好像什么都没有发生(因为我还没有分析过,我正在谈论控制台写入和数据库日志记录)跨度>
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-16
  • 1970-01-01
相关资源
最近更新 更多