前端《并发控制器》的实现

381 阅读4分钟

大家好,摆烂了许久,今天来点干货。今天我们介绍一下并发控制器的实现与区别。在工作中,我们经常会遇见需要同时处理大量请求的情况。为了提高速度和性能,我们会采取并发的形式控制请求。

(1)并发和并行的区别

首先,我们先来讲一下并发和并行的概念。

  1. 并发:“并发是指在一段时间内,多个任务快速交替地执行,每个任务只有一小段时间被处理,看起来像是同时进行”。
  2. 并行:“并行则是真正意义上的同时执行,需要多核CPU或者分布式系统支持”。

简单来说,并发强调的是“看起来”同时执行,而并行则强调的是“真正”的同时执行。

而“并发”这种方式可以提高程序的效率和性能,但需要注意控制并发量避免出现问题。为了实现控制并发量,接下来我们来实现一个基于p-limit源码的并发控制器。只需要简单的几十句js代码就可以实现。

我们来看一下并发量控制为2的情况下的运行效果。

我来解释一下图中的运行,每次运行的任务队列是2,当任务队列中有优先完成的任务,把完成的任务移出队,然后把未完成的任务进队。保持运行的任务队列一直为2,直到所有任务完成。

(2)并发控制 和 切片控制的区别

当然,也有人把切片控制来实现控制请求量,但是切片控制跟并发控制是两个区别。切片控制是把所有任务队列等份切割,然后等待每一份小任务队列执行完,再执行下一个小任务队列。切片控制的弊端就是,切割后的小任务队列里,如果有任务的执行时间很长,那么就需要等待任务执行完成才能进行下一个小任务队列。 下面我们来看一下并发控制和切片控制的执行效果。

  • 切片控制效果

  • 并发控制效果

切片耗时了10s左右,而并发耗时了8s左右。

(3)并发控制器的实现

接下来,我们来实现一个并发控制器。而并发控制器的实现,就是运用异步任务和队列来实现的。下面我们采用函数式的方式实现,如果需要ES6的类来实现的话,可以在后面查看源码地址。

  1. 首先我们先建立一个函数,返回一个收集要执行任务的构造函数。构造函数创建一个异步任务,如new一个Promise。
const ConcurrencyController = function (maxConcurrency) {

    // 1. 收集任务并返回一个Promise
    const generator = function (fn, ...args) {
        return new Promise((resolve) => {
            enqueue(fn, args, resolve);
        })
    }

    return generator;
}

  1. 初始化队列和编写入队函数
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;
}

  1. 实现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 = [];
            }
        }
    });

现在我们已经实现好了一个并发控制器了。你可以在此核心代码上,增加更多的特性来满足需求。