实现一个 Promise 调度器,要求:
-
支持限制最大并发数
limit
-
支持动态添加任务
add(taskFactory)
-
add
返回Promise
,在任务执行完成后 resolve -
调度器保证:
- 如果当前运行数
< limit
,立即执行 - 否则进入队列,等待有任务结束再调度
- 如果当前运行数
典型应用:接口限流、分片上传、大规模爬虫请求
运行示例
js
const scheduler = new Scheduler(2);
scheduler.add(() => timeout(1000).then(() => console.log('1')));
scheduler.add(() => timeout(500).then(() => console.log('2')));
scheduler.add(() => timeout(300).then(() => console.log('3')));
scheduler.add(() => timeout(400).then(() => console.log('4')));
// 输出顺序应为:2 3 1 4
基础版
最简单的想法:用一个 running
变量统计当前正在执行的任务数,如果 < limit
就执行,否则排队
ts
class Scheduler {
limit:number;
running:0;
queue:Array<()=>void>=[];
constructor(limit:number){
this.limit=limit
}
add (taskFactory:()=>Promise<any>):Promise<any>{
return new Promise((resolve,reject)=>{
const run = async ()=>{
this.running++
try{
const result=taskFactory()
resolve(result)
} catch(err) {
reject(err)
} finally {
this.running--
this.next()
}
};
if(this.running<this.limit){
run()
}else {
this.queue.push(run)
}
})
}
private next(){
if(this.running>=this.limit) return
const task=this.queue.shift()
task && task()
}
}
进阶版:并发调度+保序
在很多场景(如批量请求数据),我们需要 保持任务结果的顺序 。比如任务完成顺序是 B-C-A
,但输出结果要保证是 [A, B, C]
。
思路:
- 在
add
时给每个任务分配一个索引 - 用数组
results
按索引存储结果 - 最终按原顺序返回结果
ts
class SchedulerWithOrder {
private limit: number;
private running = 0;
private queue: Array<() => void> = [];
private results: any[] = [];
constructor(limit: number) {
this.limit = limit;
}
add<T>(taskFactory: () => Promise<T>, index: number): Promise<T> {
return new Promise<T>((resolve, reject) => {
const run = async () => {
this.running++;
try {
const result = await taskFactory();
this.results[index] = result;
resolve(result);
} catch (err) {
reject(err);
} finally {
this.running--;
this.next();
}
};
if (this.running < this.limit) {
run();
} else {
this.queue.push(run);
}
});
}
private next() {
if (this.running >= this.limit) return;
const task = this.queue.shift();
task && task();
}
getResults() {
return this.results;
}
}
高级版(超时/取消/错误策略)
实际工程中,还会有更多需求:
- 任务超时:防止任务卡死
- 任务取消 :类似
AbortController
- 错误策略:失败是否中断?还是跳过继续?
ts
class AdvancedScheduler {
private limit: number;
private running = 0;
private queue: Array<() => void> = [];
constructor(limit: number) {
this.limit = limit;
}
add<T>(taskFactory:()=>Promise<T>,timeout=4000):Promise<T>{
let timer: any;
let rejectFn: (reason?: any) => void;
const promise = new Promise<T>((resolve, reject) => {
rejectFn = reject;
const run = async () => {
this.running++;
try {
const result = await Promise.race([
taskFactory(),
new Promise<T>((_, rej) => {
timer = setTimeout(() => rej(new Error('Task timeout')), timeout);
}),
]);
resolve(result);
} catch (err) {
reject(err);
} finally {
clearTimeout(timer);
this.running--;
this.next();
}
};
if (this.running < this.limit) {
run();
} else {
this.queue.push(run);
}
});
// 支持取消
(promise as any).cancel = () => {
rejectFn(new Error('Task cancelled'));
};
return promise;
}
private next(){
if(this.running>=this.limit) return
const task=queue.shift()
task && task()
}
}
测试用例
ts
const scheduler = new Scheduler(2);
const delay = (ms: number, label: string) => () =>
new Promise(resolve => {
setTimeout(() => {
console.log(`Task ${label} done`);
resolve(label);
}, ms);
});
async function test() {
const results = await Promise.all([
scheduler.add(delay(1000, 'A')),
scheduler.add(delay(500, 'B')),
scheduler.add(delay(300, 'C')),
scheduler.add(delay(400, 'D')),
]);
console.log('all done:', results);
}
test();