[英]Parallelizing a task using .AsParallel().ForAll or Parallel.ForEach performance issue
我有一个网站列表和一个代理服务器列表。
我有这个动作
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分钟来完成其余的工作。
在连接线程时,这看起来像是一个问题,但我无法发现这可能是什么。
您正在浪费线程和CPU时间。 在这种情况下,您将有12个线程。 每个线程一次只能处理一个网址。 因此,您一次只能处理12个网址。 而且,在大多数情况下,这些线程什么都不做(它们只会等待免费的代理或已加载的页面),而它们却可以用于执行更多有用的任务。
为避免这种情况,您应该使用非阻塞IO操作。 因此,而不是使用htmlDocumentLoader.LoadDocument
你应该考虑使用它的异步接口之一( htmlDocumentLoader.BeginLoadDocument
/ htmlDocumentLoader.EndLoadDocument
或htmlDocumentLoader.LoadDocumentAsync
/ htmlDocumentLoader.LoadDocumentCompleted
)。 在这种情况下,如果您有100个URL,则将同时加载所有这些URL,而不会创建额外的线程并浪费CPU时间。 只有在加载页面时,才会创建新线程(实际上是从ThreadPool中获取)来处理它。
您等待免费代理的方式也很浪费。 与其使用while (ProxyList.Count == 0)
在没有免费代理的情况下冻结线程的情况, while (ProxyList.Count == 0)
考虑使用计时器,它会每秒唤醒一次,并检查免费代理是否可用。 它不是最佳解决方案,但至少不会浪费线程。 更好的解决方案是向ProxyHandler添加事件,该事件将在代理可用时通知。
您的问题可能是由于PLinq使用了分区程序。
如果使用Range Partitiner,则您的URL集合将分为几组,每组中的URL数目相等。 然后,为每个组启动任务,而无需进一步同步。
这意味着,当所有其他任务完成时,将有一个任务耗时最长,并且仍有工作要做。 这实际上意味着该操作的最后部分是单线程的。
解决方案是使用其他分区程序。 您可以按照MSDN上的说明使用内置的Chunk Partitioner。
如果这样做还不够好,您将不得不编写/查找一个分区生成器,该生成器会逐个生成元素。 这内置于C#5: EnumerablePartitionerOptions
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.