经典面试题:控制并发

100 阅读3分钟

前言

我们今天来聊一聊大厂面试时经常会问的一个问题——控制并发,那什么是控制并发呢?就是我们在处理异步操作时,我们通过控制任务的最大并发数来控制多个异步任务能够同时进行。

实现原理

我们来通过上面对控制并发实现的过程来尝试自己手写完成一份控制并发实现

// 我们通过ajax来模拟异步请求的函数
function ajax(time) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve();
        }, time);
    });
    
    // 添加任务的函数
function addTask(time, name) {
    limit
        .add(() => ajax(time))
        .then(() => {
            console.log(`任务${name}完成`);
        })
        .catch(() => {
            console.log(`任务${name}失败`);
        });
}

/ 控制并发数的类
class Limit {
    // 构造函数,初始化任务队列、正在运行的任务数和最大并发数
    constructor(paralleCount = 2) {
        this.tasks = [];
        this.runningCount = 0;
        this.paralleCount = paralleCount;
    }
  }

// 创建一个并发数为 2 的 Limit 实例
const limit = new Limit(2);


// 添加任务
addTask(10000, 1);
addTask(5000, 2);
addTask(1000, 3);
addTask(4000, 4);
addTask(7000, 5);

我们先通过ajax来模拟一个异步请求定时器函数,同时我们要定义一个并发类,并给其传参控制并发数最大为2,然后我们再通过addTask函数中的add方法来实现将任务添加至任务队列中,并在添加后对其进行触发。那现在我们就要时间add方法

add方法实现

我们根据上面写的主函数的方式发现在控制并发对类中的异步操作在添加后就在add()方法内部进行了执行

  // 添加任务的方法
    add(task) {
        return new Promise((resolve, reject) => {
            // 将任务、成功回调和失败回调添加到任务队列中
            this.tasks.push({
                task,
                resolve,
                reject
            });
            // 尝试运行任务
            this._run();
        });
    }

因为我们通过add()方法传入到limit的实例中的是一个Promise我们将这个任务连同其resolve和reject都传入到这个队列当中去,并通过创建的_run方法来对其触发。

run方法的实现

// 执行任务的方法
    _run() {
        // 当正在运行的任务数小于最大并发数且任务队列不为空时
        while (this.runningCount < this.paralleCount && this.tasks.length) {
            // 从任务队列中取出一个任务
            const { task, resolve, reject } = this.tasks.shift();
            // 正在运行的任务数加 1
            this.runningCount++;
            // 执行任务,并处理成功和失败的回调
            task().then(resolve, reject).finally(() => {
                // 任务完成后,正在运行的任务数减 1
                this.runningCount--;
                // 再次尝试运行任务
                this._run();
            });
        }
    }

我们将队列中要执行的方法拿出来进行触发,通过Promise对象自身所带finally方法来实现

  • finally 方法是一个用于无论 Promise 最终状态是 fulfilled 还是 rejected,都会执行指定逻辑的工具。它的核心特点是不关心 Promise 的结果,只关心操作是否完成

最终代码实现

// 模拟异步请求的函数
function ajax(time) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve();
        }, time);
    });
}

// 控制并发数的类
class Limit {
    // 构造函数,初始化任务队列、正在运行的任务数和最大并发数
    constructor(paralleCount = 2) {
        this.tasks = [];
        this.runningCount = 0;
        this.paralleCount = paralleCount;
    }

    // 添加任务的方法
    add(task) {
        return new Promise((resolve, reject) => {
            // 将任务、成功回调和失败回调添加到任务队列中
            this.tasks.push({
                task,
                resolve,
                reject
            });
            // 尝试运行任务
            this._run();
        });
    }

    // 执行任务的方法
    _run() {
        // 当正在运行的任务数小于最大并发数且任务队列不为空时
        while (this.runningCount < this.paralleCount && this.tasks.length) {
            // 从任务队列中取出一个任务
            const { task, resolve, reject } = this.tasks.shift();
            // 正在运行的任务数加 1
            this.runningCount++;
            // 执行任务,并处理成功和失败的回调
            task().then(resolve, reject).finally(() => {
                // 任务完成后,正在运行的任务数减 1
                this.runningCount--;
                // 再次尝试运行任务
                this._run();
            });
        }
    }
}

// 创建一个并发数为 2 的 Limit 实例
const limit = new Limit(2);

// 添加任务的函数
function addTask(time, name) {
    limit
        .add(() => ajax(time))
        .then(() => {
            console.log(`任务${name}完成`);
        })
        .catch(() => {
            console.log(`任务${name}失败`);
        });
}

// 添加任务
addTask(10000, 1);
addTask(5000, 2);
addTask(1000, 3);
addTask(4000, 4);
addTask(7000, 5);

由此我们来实现对异步代码之间的控制并发