大家好,摆烂了许久,今天来点干货。今天我们介绍一下并发控制器的实现与区别。在工作中,我们经常会遇见需要同时处理大量请求的情况。为了提高速度和性能,我们会采取并发的形式控制请求。
(1)并发和并行的区别
首先,我们先来讲一下并发和并行的概念。
- 并发:“并发是指在一段时间内,多个任务快速交替地执行,每个任务只有一小段时间被处理,看起来像是同时进行”。
- 并行:“并行则是真正意义上的同时执行,需要多核CPU或者分布式系统支持”。
简单来说,并发强调的是“看起来”同时执行,而并行则强调的是“真正”的同时执行。
而“并发”这种方式可以提高程序的效率和性能,但需要注意控制并发量避免出现问题。为了实现控制并发量,接下来我们来实现一个基于p-limit源码的并发控制器。只需要简单的几十句js代码就可以实现。
我们来看一下并发量控制为2的情况下的运行效果。
我来解释一下图中的运行,每次运行的任务队列是2,当任务队列中有优先完成的任务,把完成的任务移出队,然后把未完成的任务进队。保持运行的任务队列一直为2,直到所有任务完成。
(2)并发控制 和 切片控制的区别
当然,也有人把切片控制来实现控制请求量,但是切片控制跟并发控制是两个区别。切片控制是把所有任务队列等份切割,然后等待每一份小任务队列执行完,再执行下一个小任务队列。切片控制的弊端就是,切割后的小任务队列里,如果有任务的执行时间很长,那么就需要等待任务执行完成才能进行下一个小任务队列。 下面我们来看一下并发控制和切片控制的执行效果。
- 切片控制效果
- 并发控制效果
切片耗时了10s左右,而并发耗时了8s左右。
(3)并发控制器的实现
接下来,我们来实现一个并发控制器。而并发控制器的实现,就是运用异步任务和队列来实现的。下面我们采用函数式的方式实现,如果需要ES6的类来实现的话,可以在后面查看源码地址。
- 首先我们先建立一个函数,返回一个收集要执行任务的构造函数。构造函数创建一个异步任务,如new一个Promise。
const ConcurrencyController = function (maxConcurrency) {
// 1. 收集任务并返回一个Promise
const generator = function (fn, ...args) {
return new Promise((resolve) => {
enqueue(fn, args, resolve);
})
}
return generator;
}
- 初始化队列和编写入队函数
const ConcurrencyController = function (maxConcurrency) {
let runningCount = 0; // 当前运行数
let queue = []; // 任务队列
// 2. 入队,等待执行
const enqueue = (fn, args, resolve) => {
queue.push(runTask.bind(null, fn, args, resolve));
(async () => {
// 该微任务是用来等待,所有任务(fn)进入队列后, 再开始执行
await Promise.resolve();
// 3. 当前满足条件,可以先执行任务
if (runningCount < maxConcurrency && queue.length > 0) {
const task = queue.shift();
task();
}
})()
}
// 1. 收集任务并返回一个Promise
const generator = function (fn, ...args) {
return new Promise((resolve) => {
enqueue(fn, args, resolve);
})
}
return generator;
}
- 实现runTask方法来执行每个任务
const ConcurrencyController = function (maxConcurrency) {
let runningCount = 0; // 当前运行数
let queue = []; // 任务队列
// 5. 执行下一个任务
const nextTask = () => {
runningCount--;
if (queue.length > 0) {
const task = queue.shift();
task();
}
}
// 4. 开始跑任务
const runTask = async (fn, args, resolve) => {
runningCount++;
const result = (async () => fn(...args))();
resolve(result);
try {
await result;
} catch {
}
nextTask();
}
// 2. 入队,等待执行
const enqueue = (fn, args, resolve) => {
queue.push(runTask.bind(null, fn, args, resolve));
(async () => {
// 该微任务是用来等待,所有任务(fn)进入队列后, 再开始执行
await Promise.resolve();
// 3. 当前满足条件,可以先执行任务
if (runningCount < maxConcurrency && queue.length > 0) {
const task = queue.shift();
task();
}
})()
}
// 1. 收集任务并返回一个Promise
const generator = function (fn, ...args) {
return new Promise((resolve) => {
enqueue(fn, args, resolve);
})
}
return generator;
}
到这里,我们就已经实现好一个并发控制器了,接下来,我们整点数据测试是否正常跑起来。简单的加一些setTimeout来处理一下。
为了更好的开发条件,我们需要获取出内部的运行情况,这时可以加一些只读的属性,如正在运行的数量,任务队列长度,清除任务队列等方法。可以采用Object.defineProperties方法来实现。
// 6. 增加一些可读属性
Object.defineProperties(generator, {
runningCount: {
get: () => runningCount,
},
queueSize: {
get: () => queue.length,
},
clearQueue: {
value: () => {
queue = [];
}
}
});
现在我们已经实现好了一个并发控制器了。你可以在此核心代码上,增加更多的特性来满足需求。