使用.AsParallel()。ForAll或Parallel.ForEach性能问题并行化任务

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

我有这个动作

Action 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 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个url中,花费尽可能多的时间来完成前90个,就像在过去的10个网站中那样。

我已经排除代理已经死亡,因为没有抛出任何exception。 看起来好像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分钟。

这似乎是加入Threads时的问题,但我没有发现它可能是什么。

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

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

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

您的问题可能是由于PLinq使用了分区程序。

如果正在使用Range Partitiner,则您的url集合将分成每组中具有相等(ish)数量的url的组。 然后,为每个组启动任务,没有进一步的同步。

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

解决方案是使用不同的分区程序。 您可以使用内置的Chunk Partitioner,如MSDN上所述 。

如果这不能很好地工作,你将不得不编写/找到一个逐个产生元素的分区器实现。 这内置于C#5: EnumerablePartitionerOptions