设计线程池时,本质上所使用的逻辑模型仍然是我们熟悉的“生产者/消费者”模型。
外部线程负责产生需要执行的任务,线程池线程负责执行这些任务。任务放在一个共享的数据结构中,通常是一个线程安全的队列。
生产 消费
外部线程(生产者)--->任务<---线程池线程(消费者)
通常,任务对象会提供一个run()方法,用于外部调用者执行任务。
最近看JDK1.5的并发功能,发现我以前在Windows编程中实现线程池时,思路存在问题,造成了程序复杂度增加。
下面整理一下线程池的实现方法,比较一下我以前的实现思路和JDK的实现思路。
*我以前实现线程池的思路
********************************************************************************
一、基本思路
线程池内部维护一个线程数组。
外部线程和线程池线程共享一个任务队列。
如果是Windows编程,队列中可以设置一个信标(Semephore),表示队列中元素的个数,初始值为0,最大值为队列的上限。假设这个信标命名为QueueSize。
线程池在执行任务时,会在内部调用task.run()方法。
外部线程的执行流程:
1、产生任务;
2、则调用队列queue.add(task)将任务添加到队列中;
3、如果需要,重复以上过程。
线程池线程的执行流程:
1、检查任务队列;
2、调用队列queue.remove()将任务重队列中取出;
3、执行上一步得到的任务;
4、反复以上过程。
队列的实现:
1、队列的add(task)方法实现时,递增QueueSize;
function void queue::add(task) {
//如果达到队列上限,则线程被阻塞
ReleaseSemaphore(QueueSize);
//同步修改队列
EnterCriticalSection(cs);
_queue.add(task);
LeaveCriticalSection(cs);
return;
}
2、队列的remove()方法实现时,递减QueueSize;
function &task queue::remove() {
//如果queueSize==0,则线程被阻塞
WaitForSingleObject(QueueSize);
//同步修改队列
EnterCriticalSection(cs);
task &ret = _queue.remove();
LeaveCriticalSection(cs);
return ret;
}
二、应用时的伪代码。
线程池的创建,通常在主线程中进行:
queue = new TaskQueue(queueSize);
pool = new ThreadPool(queue, poolSize);
某个外部线程的执行片段:
//...
task = new Task();
//...
queue.add(task);
三、小结
用上述思路实现的线程池,实现了线程池的功能的同时,也提供了任务队列缓存功能,看起来似乎不错。
但仔细思考我们就会发现,线程池的内部实现调用了任务队列的操作接口,使得线程池和队列偶合了起来。
※JDK实现线程池的思路
********************************************************************************
一、基本思路
线程池内部维护一个线程数组。线程池对外提供一个execute(task)接口,内部回调task.run()方法。
外部线程的执行流程:
1、产生任务;
2、把直接交给线程池执行,当然也可以交给一个任务管理器来执行;
pool.execute(task);
3、如果需要,重复上述步骤。
线程池的执行过程:
1、如果有任务且池中有空余线程,则执行任务;
2、否则,池中线程阻塞等待;
3、重复以上过程。
二、应用时的伪代码:
线程池的创建:
pool = new ThreadPool(poolSize);
某个外部线程的执行片段:
//...
task = new Task();
//...
pool.execute(task);
queue = new TaskQueue(queueSize);
三、小结
可以发现,JDK的线程池的实现时,做到了完全与队列剥离。不但降低了复杂度,而且容易通过编写外部的任务管理器来调度线程。
JDK线程池的设计,与我以前自己设计的线程池相比,更符合SRP(单一职责原则)和低耦合原则,代码也更容易维护。值得好好学习!