一文总结 Promise 并发题

195 阅读3分钟

一文总结 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方法继续执行下一个异步任务。