注意标题里面有三个keyword,递归 线程池 队列
这是我在开发爬虫程序时候碰到的需求,我们知道爬网页一般都使用广度优先搜索,每个页面下面都可能有下层页面。为了加快爬行速度,我们很容易想到用多线程来实现。
但是多线程必须受控,不能无限的创建线程,这样机器受不了,服务器也受不了。所以多线程必须满足下面条件:
线程数限制,超过限制则加入到队列中等待。同时,线程中允许创建新的线程,我称之为递归线程。
根据这个需求,所以有了标题:递归线程池队列
下面是实现代码
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
namespace MediaSpider {
class TaskQueue {
int poolSize;
Task[] pool;
ConcurrentQueue<Action> queue;
CancellationTokenSource cancelTokenSource=null;
public TaskQueue(int poolSize) {
this.poolSize = poolSize;
// 初始化线程池
pool = new Task[poolSize];
for (int i = 0; i < poolSize; i++) {
pool[i] = Task.Run(() => { });
}
queue = new ConcurrentQueue<Action>();
}
public void Add(Action action) {
queue.Enqueue(action);
if(cancelTokenSource!=null) cancelTokenSource.Cancel();
}
public void Wait() {
while (true)
{
cancelTokenSource = null;
int index = Task.WaitAny(pool);
// 如果队列不为空,则加入线程池执行
if (queue.TryDequeue(out Action action))
{
pool[index] = Task.Run(action);
continue;
}
cancelTokenSource = new CancellationTokenSource();
try
{
// 等待线程池所有线程都执行完毕
Task.WaitAll(pool, cancelTokenSource.Token);
}
catch (OperationCanceledException)
{
// 如果队列追加,则执行队列的任务
continue;
}
// 如果都执行完毕,则退出等待
return;
}
}
}
}
调用方法
pool = new TaskQueue(6);//初始化线程池,大小为6
pool.Add(() => { Task.Delay(100); });//添加任务
pool.Add(() => { Task.Delay(100); });//添加任务
pool.Add(() => { Task.Delay(100); });//添加任务
pool.Wait();//等待任务执行完毕
实现的思路如下:
主线程只是守护线程,任务都在其他线程里面实现。
守护线程(Wait)实现下面的功能
1.线程调度:当线程池有空闲的时候,把队列中的线程加入线程池执行
2.等待全部线程池和队列执行完毕
实现中遇到的问题
必须等待线程池和队列同时为空才能算执行完毕,线程也会不停的创建新的线程。
如果没有递归就比较好办,有了递归,队列都执行完了,线程池中的线程还可以添加队列。不能够单纯的WaitAll。想了几个方式,都有些问题。后来发现WaitAll(Task[], CancellationToken)是可以被打断的,才最终解决这个问题。
如果不需要递归,那么用普通的WaitAll就可以了,代码还可以简化。
任务队列刚开始是用Queue来实现的,由于不是线程安全需要Lock来互斥,后来发现线程安全的ConcurrentQueue,就把Lock去掉,提高了性能