我正在为一个网站开发一个多线程的scraper,根据一个不同的问题,我决定将ThreadPool与QueueUserWorkItem()一起使用。
如何在不对所有工作项进行排队的情况下不断对工作项进行排队?我需要排队> 300k项目(每个用户ID一个),如果我循环排队,我将耗尽内存。
所以,我想要的是:
// 1 = startUserID, 300000 = endUserID, 25 = MaxThreads
Scraper webScraper = new Scraper(1, 300000, 25);
webScraper.Start();
// return immediately while webScraper runs in the background
在此期间,当线程可用时,webScraper会连续添加所有300000个workItem。
这是我到目前为止所做的:
public class Scraper
{
private int MaxUserID { get; set; }
private int MaxThreads { get; set; }
private static int CurrentUserID { get; set; }
private bool Running { get; set; }
private Parser StatsParser = new Parser();
public Scraper()
: this(0, Int32.MaxValue, 25)
{
}
public Scraper(int CurrentUserID, int MaxUserID, int MaxThreads)
{
this.CurrentUserID = CurrentUserID;
this.MaxUserID = MaxUserID;
this.MaxThreads = MaxThreads;
this.Running = false;
ThreadPool.SetMaxThreads(MaxThreads, MaxThreads);
}
public void Start()
{
int availableThreads;
// Need to start a new thread to spawn the new WorkItems so Start() will return right away?
while (Running)
{
// if (!CurrentUserID >= MaxUserID)
// {
// while (availableThreads > 0)
// {
// ThreadPool.QueueUserWorkItem(new WaitCallBack(Process));
// }
// }
// else
// { Running = false; }
}
}
public void Stop()
{
Running = false;
}
public static void process(object state)
{
var userID = Interlocked.Increment(ref CurrentUserID);
... Fetch Stats for userID
}
}
这是正确的做法吗?
一旦调用了Start()并且没有立即创建所有工作项,是否可以指导我在正确的方向上处理创建我的工作项目的背景?
答案 0 :(得分:2)
使用较少的工作项从工作队列中窃取工作会更好地实现吗?仅仅因为你有300,000件工作要做,并不意味着你需要300,000名工人来做。显然,由于你只有几个核心,这些工作中只有少数可以并行发生,那么为什么不向更少的工人分发工作呢?
根据每项工作所花费的时间的不变,您可以将其全部均匀地分配到每个工作人员中,或者拥有一个中央队列(您必须锁定)并且每个工作人员都可以抓住一些工作。它耗尽了。
编辑:
Joe Duffy似乎有一篇关于在这里编写工作窃取队列的系列文章:http://www.bluebytesoftware.com/blog/2008/08/12/BuildingACustomThreadPoolSeriesPart2AWorkStealingQueue.aspx。它看起来也像.Net 4的Threadpool会更聪明一些。但我不认为你需要一些特别复杂的东西。
答案 1 :(得分:0)
我认为创建一个排队项目队列似乎不太合适,那么如何在完成后再让WorkItems自己排队?
您的Start方法可以排队,例如,MaxThreads项目的3倍(在您的示例中为75),然后您的Process方法在完成后排队。这样你的Start方法可以快速返回,但会触发许多工作项,正如我所说的那样,然后自行解雇:
public class Scraper
{
private int MaxUserID { get; set; }
private int MaxThreads { get; set; }
private int currentUserID;
private bool Running { get; set; }
private Parser StatsParser = new Parser();
private int Multiplier { get; set; }
public Scraper()
: this(0, Int32.MaxValue, 25)
{
}
public Scraper(int currentUserID, int maxUserID, int maxThreads)
{
this.currentUserID = currentUserID;
this.MaxUserID = maxUserID;
this.MaxThreads = maxThreads;
this.Running = false;
ThreadPool.SetMaxThreads(maxThreads, maxThreads);
Multiplier = 3;
}
public void Start()
{
Running = true;
for (int i = 0; i < MaxThreads * Multiplier; i++)
{
ThreadPool.QueueUserWorkItem(Process);
}
}
public void Stop()
{
Running = false;
}
public void Process(object state)
{
if (Running == false)
{
return;
}
if (currentUserID < MaxUserID)
{
Interlocked.Increment(ref currentUserID);
//Parse stats for currentUserID
ThreadPool.QueueUserWorkItem(Process);
}
else
{ Running = false; }
}
}
我确定应该使用Interlocked设置Running标志以确保安全。我已经将乘数变成了一个属性,可以传递给构造函数 - 我很确定它可以调整以调整性能,具体取决于这些统计数据需要多长时间来解析。
答案 2 :(得分:0)
我绝对不会使用ThreadPool.SetMaxThreads - 记住线程池是在所有进程之间共享的 - 设置最大线程数会简单地扼杀性能。线程池背后的整个想法是你不需要指定最大线程数量的东西 - .Net框架计算出要分配的最佳线程数量 - 你不需要这样做。
请注意,排队30万个项目不会导致300,000个线程产生 - ThreadPool类将为您管理线程数并根据需要重新使用线程。如果您只是担心会以这种方式消耗太多资源,我建议您优化您的流程 - 也许创建一个'Spawner'类,然后运行1000个scraper实例?
答案 3 :(得分:0)
看起来你需要一个主过程控制类来控制正在开火的工人数量并保持队列满。
您可以使用两个队列:
然后,此Master / Governor对象将保持循环,直到队列#1中的所有项目都消失,并且当您有可用的周期时,它将继续添加到队列#2。
答案 4 :(得分:0)
您可以使用其他线程池。这是一个:http://www.codeplex.com/smartthreadpool 它允许您一次排队所有项目。您可以指定要创建的最大线程数。假设您有1000个工作项并且分配了100个线程。它将立即采取前100项,并让其他人等待。只要其中一个项目完成并且线程释放,就会启动下一个排队的项目。它管理所有工作,但不会使线程和内存饱和。此外,它不使用.net线程池中的线程。