一文总结 Promise 并发题
原题
前两天做到一道某大厂的题,需要实现一个Promise并发,和之前做过的Promise并发题的形式不太一样(后来发现是用命令模式设计的),在这里记录一下。
// 原题
class Solution {
limit // 并发限制数
queue = [] // 任务队列
constructor(limit) {
this.limit = limit
}
enqueue(task) {
// TODO
}
_next() {
// TODO
}
}
// 题目用例
const s = new Solution(2)
let tasks = [
() => new Promise((resolve, reject) => setTimeout(() => resolve(1), 500)),
() => new Promise((resolve, reject) => setTimeout(() => resolve(2), 1000)),
() => new Promise((resolve, reject) => setTimeout(() => resolve(3), 1500)),
() => new Promise((resolve, reject) => setTimeout(() => resolve(4), 2000)),
]
tasks = tasks.map(task => s.enqueue(task))
tasks[2].then(v => console.log(v))
Promise.all(tasks).then(v => console.log(v))
原题的目的是实现enqueue和_next方法。和常见的promise并发题目不同的地方在于,这个场景下可以通过enqueue方法将promise添加到队列中,以往的并发题目队列往往是固定的。
通过用例可知enqueue方法的目的是把promise入队列,并返回一个promise,如果运行的promise数量达到了并发限制数,则不运行该promise,而是等待调度。
而_next方法题目中只说是负责调度逻辑,具体如何实现需要我们自己设计。
由于好久没做这种题了,当时也没做出来,下来之后把题目做了出来,参考答案如下:
class Solution {
limit // 并发限制数
queue = [] // 任务队列
running = 0 // 正在运行的任务数
constructor(limit) {
this.limit = limit
}
enqueue(task) {
// 返回一个新的 promise,用于包装原始的 task
return new Promise((resolve, reject) => {
// 把 resolve 和 reject 函数存入队列,以便后续调用
this.queue.push({ task, resolve, reject })
// 尝试执行下一个任务
this._next()
})
}
_next() {
// 如果队列为空,或者达到并发限制,就不执行任何操作
if (this.queue.length === 0 || this.running >= this.limit) return
// 出队一个任务对象
const { task, resolve, reject } = this.queue.shift()
// 增加运行任务数
this.running++
// 执行 task,并用 then 和 catch 处理结果
// 注意这里才调用 task,因为是要把 promise 的执行延后到出队列时间点
task()
.then((value) => {
// 调用 resolve 函数,传递 value 给外部的 promise
resolve(value)
// 减少运行任务数
this.running--
// 尝试执行下一个任务
this._next()
})
.catch((error) => {
// 调用 reject 函数,传递 error 给外部的 promise
reject(error)
// 减少运行任务数
this.running--
// 尝试执行下一个任务
this._next()
})
}
}
// 题目用例
const s = new Solution(2)
let tasks = [
() => new Promise((resolve, reject) => setTimeout(() => resolve(1), 500)),
() => new Promise((resolve, reject) => setTimeout(() => resolve(2), 1000)),
() => new Promise((resolve, reject) => setTimeout(() => resolve(3), 1500)),
() => new Promise((resolve, reject) => setTimeout(() => resolve(4), 2000)),
]
tasks = tasks.map(task => s.enqueue(task))
tasks[2].then(v => console.log(v))
Promise.all(tasks).then(v => console.log(v))
扩展
用同样的思路,可以解决其他类型的并发题目。
比如下面这道题,需要我们写一个装饰器函数limitConcurrency,从而在对异步函数f进行并发数限制的同时,又可以降低对函数的侵入性
type AsyncFunction = (...args: any[]) => Promise<any>
// 装饰器函数实现
function limitConcurrency(f: AsyncFunction, limit: number): AsyncFunction {
// YOUR CODE HERE
}
// 测试
const fetchFunction: AsyncFunction = (url) => {
return new Promise(resolve => {
setTimeout(() => {
resolve(url)
}, 1000)
})
}
const limitedFetch = limitConcurrency(fetchFunction, 2)
const urls = ['1', '2', '3', '4', '5']
const promises = urls.map(url => limitedFetch(url))
Promise.all(promises).then().catch()
使用和原题类似的思路,我们可以得出答案:
type AsyncFunction = (...args: any[]) => Promise<any>
// 装饰器函数实现
function limitConcurrency(f: AsyncFunction, limit: number): AsyncFunction {
// YOUR CODE HERE
const queue: any[] = []
let running = 0
function next() {
// 如果队列为空,或者达到并发限制,就不执行任何操作
if (queue.length === 0 || running >= limit ) return
const { args, resolve, reject } = queue.shift()
running++
// 执行异步任务
f(...args).then((result: any) => {
resolve(result)
console.log(result) // 打印一下结果
running--
next() // 尝试调用下一个任务
}).catch((err: any) => {
reject(err)
running--
next()
})
}
return function(...args) {
return new Promise((resolve, reject) => {
queue.push({
args,
resolve,
reject
})
next()
})
}
}
// 测试
const fetchFunction: AsyncFunction = (url) => {
return new Promise(resolve => {
setTimeout(() => {
resolve(url)
}, 1000)
})
}
const limitedFetch = limitConcurrency(fetchFunction, 2)
const urls = ['1', '2', '3', '4', '5']
const promises = urls.map(url => limitedFetch(url))
Promise.all(promises).then().catch()
总结
本文总结了一种Promise并发题解决的通用思路,即使用queue存放要执行的异步任务,通过next方法调用队列中的异步任务,如果并发数达到限制或队列为空则不执行,当异步任务执行完毕后调用next方法继续执行下一个异步任务。