这是我参与8月更文挑战的第15天,活动详情查看:8月更文挑战
Master-Worker 模式是一种常见的高并发模式,它的核心思想,任务的调度和执行分离,调度任务的角色为 Master,执行任务的角色为 Worker,Master 负责接收和、分配任务和合并(Merge) 任务结果,Worker 进程负责执行任务。Master-Worker 模式是一种归并类型的模式。
Master-Worker架构
-
master中有两个属性,一个是存放需要执行的任务,另一个是任务调度线程,专门从任务队列中获取任务并派发给worker线程。
-
worker中也有两个属性,一个是存放需要执行的任务,另一个是执行任务的线程,执行任务的线程不断的从阻塞队列中获取任务并执行。
代码实现
因为涉及代码到封装性,所有的代码最终都会放到同一个包中,利用包特有的访问权限对一些方法实现访问权限限制,只允许包内的类进行访问。
Task封装
-
task中核心的属性有taskId,workerId,任务执行后的结果以及对结果的异步回调。
-
通过抽象方法
exec交给用户实现具体的任务 -
每个任务都会通过
WORKER_ID_GENERATOR生成一个唯一的taskId
public abstract class Task<R> {
private static final AtomicInteger WORKER_ID_GENERATOR = new AtomicInteger(0);
/**
* 任务id
*/
private final int taskId;
/**
* 执行任务的work线程Id
*/
@Setter
private int workerId;
/**
* 任务执行结果
*/
private R result;
/**
* 任务执行完后,对结果的回调
*/
private Consumer<R> resultAction;
public Task() {
this.taskId = WORKER_ID_GENERATOR.getAndIncrement();
}
/**
* 执行任务,执行完任务后并进行回调
*/
void execute() {
this.result = this.exec();
if (resultAction != null) {
resultAction.accept(this.result);
}
System.out.println(toString());
}
/**
* 执行任务
*
* @return R
*/
protected abstract R exec();
@Override
public String toString() {
return "Task{" +
"taskId=" + taskId +
", workerId=" + workerId +
", result=" + result +
'}';
}
void setResultAction(Consumer<R> resultAction) {
this.resultAction = resultAction;
}
}
Worker
Worker 接收 Master 分配的任务,同样也通过阻塞队列对局部任务进行缓存。Worker 所拥有 的线程作为局部任务的阻塞队列的消费者,不断从阻塞队列获取任务并且执行,执行完成后回调 Master 传递过来的回调函数。
public class Worker<R> {
private final ArrayBlockingQueue<Task<R>> taskQueue = new ArrayBlockingQueue<>(32);
private static final AtomicInteger WORKER_ID_GENERATOR = new AtomicInteger();
private final int workerId;
private final Thread workerThread;
public Worker() {
this.workerId = WORKER_ID_GENERATOR.getAndIncrement();
workerThread = new Thread(this::execute);
workerThread.start();
}
private void execute() {
while (true) {
try {
Task<R> task = taskQueue.take();
task.setWorkerId(workerId);
task.execute();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void submit(Task<R> task) {
taskQueue.offer(task);
}
}
Master
Master 负责接收客户端提交的任务,然后通过阻塞队列对任务进行缓存。Master 所拥有的线 程作为阻塞队列的消费者,不断从阻塞队列获取任务并轮流分给 Worker。
public class Master<T extends Task<R>, R> {
private final Map<String, Worker<R>> workerMap = new HashMap<>();
private final ArrayBlockingQueue<Task<R>> taskQueue = new ArrayBlockingQueue<>(1024);
private Thread masterThread = null;
public Master(int workCount) {
for (int i = 0; i < workCount; i++) {
Worker<R> worker = new Worker<>();
workerMap.put(String.format("%s-%s", "worker", i), worker);
}
masterThread = new Thread(this::execute);
masterThread.start();
}
private void execute() {
while (true) {
workerMap.forEach((workerName, worker) -> {
try {
Task<R> task = this.taskQueue.take();
worker.submit(task);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
public void submit(T task, Consumer<R> resultAction) {
task.setResultAction(resultAction);
taskQueue.add(task);
}
}
案例实战
累加1~1000000
分析,可以对累加进行拆分成很多个子任务,每个任务负责累加一部分,将每个子任务得到的结果进行归并。
- 任务拆分
public class NumAddTask extends Task<Long> {
private final long init;
public NumAddTask(long init) {
this.init = init;
}
@Override
protected Long exec() {
long res = 0;
// 每个线程累加10万
for (int i = 0; i < 100000; i++) {
res += i + init;
}
return res;
}
}
- 需求实现
public class MasterWorkerTest {
@Test
public void test() throws InterruptedException {
AtomicLong totalCount = new AtomicLong(0);
Master<NumAddTask, Long> master = new Master<>(4);
master.submit(new NumAddTask(1), totalCount::addAndGet);
master.submit(new NumAddTask(100001), totalCount::addAndGet);
master.submit(new NumAddTask(200001), totalCount::addAndGet);
master.submit(new NumAddTask(300001), totalCount::addAndGet);
master.submit(new NumAddTask(400001), totalCount::addAndGet);
master.submit(new NumAddTask(500001), totalCount::addAndGet);
master.submit(new NumAddTask(600001), totalCount::addAndGet);
master.submit(new NumAddTask(700001), totalCount::addAndGet);
master.submit(new NumAddTask(800001), totalCount::addAndGet);
master.submit(new NumAddTask(900001), totalCount::addAndGet);
// 防止主线程退出
Thread.sleep(3000);
System.out.println("total Count = " + totalCount);
}
}
- 每个任务执行结果及其总结果
Task{taskId=0, workerId=2, result=5000050000}
Task{taskId=1, workerId=1, result=15000050000}
Task{taskId=2, workerId=0, result=25000050000}
Task{taskId=3, workerId=3, result=35000050000}
Task{taskId=7, workerId=3, result=75000050000}
Task{taskId=4, workerId=2, result=45000050000}
Task{taskId=5, workerId=1, result=55000050000}
Task{taskId=6, workerId=0, result=65000050000}
Task{taskId=8, workerId=2, result=85000050000}
Task{taskId=9, workerId=1, result=95000050000}
total Count = 500000500000
Netty中的Master-Worker模式
-
高性能传输模式——Reactor 模式,就是 Master-Worker 模式在传输领域的一种应用。
-
在 Netty 中,EventLoop 反应器内部有一个线程负责 Java NIO 选择器的事件的轮询,然后进行对应的事件 分发。事件分发(Dispatch)的目标就是 Netty 的 Handler 处理器(含用户定义的业务处理器)。
-
Netty 服务器程序中需要设置两个 EventLoopGroup 轮询组,一个组负责新连接的监听和接收,另外一个组负责 IO 传输事件的轮询与分发,两个轮询组的职责具体如下:
- 负责新连接的监听和接收的 EventLoopGroup 轮询组中的反应器(Reactor),完成查询 通道的新连接 IO 事件查询。
- 另一个轮询组中的反应器(Reactor),完成查询所有子通道的 IO 事件,并且执行对应 的 Handler 处理器完成 IO 处理——例如数据的输入和输出。
-
Netty 既是基于 Reactor 模式实现,也体现了 Master-Worker 模式的思想。Netty 的 EventLoop (Reactor 角色)可以对应到 Master-Worker 模式的 Worker 角色;而 Netty 的 EventLoopGroup 轮 询组,则可以对应到 Master-Worker 模式的 Master 角色