优雅完成高频面试题《请求并发数控制》

优雅完成高频面试题《请求并发数控制》

众所周知,大厂面试官喜欢问的问题有很多,但是有一类问题是必问的,那就是并发。并发问题是面试中的常客,而且往往是面试官的必问问题,因为并发问题是一个程序员必须要掌握的基本功,也是一个程序员的基本素养。并发问题的考察,不仅仅是考察你是否掌握了并发的基本概念,更重要的是考察你是否能够在并发的场景下,写出高效、优雅的代码。

今天,我们就来看看一个经典的并发问题:请求并发数控制

问题描述

不知道有没有同学用过 TIM,它是腾讯公司出品的一款聊天工具。但今天说的不是这款软件,而是 TIM.js, 它主要用于实时聊天的SDK。

但 TIM.js 中,有一个问题,就是它要求用户调用它的接口时,必须控制并发数:每秒最多只能调用 5 次接口。如果超过 5 次,就会报错。

这个问题,其实是一个非常经典的问题,而且在实际工作中,也经常会遇到。比如,我们在做爬虫的时候,我们也需要控制并发数,否则,我们的爬虫就会被网站封掉。

解决方案

相必大家都在网上看过很多解决方案。今天,我们引入一个很常见的概念来解决这个问题:PV操作

PV操作 和 信号量

PV操作,是操作系统中的一个概念,它是用来解决并发问题的。在操作系统中,PV操作是通过信号量来实现的。 简单来说,信号量就是一种对于某种资源的计数器,它可以用来控制对于某种资源的并发访问。而P操作和V操作,就是对于信号量的操作。分别对应着对于信号量的减少和增加。

在我们的问题中,我们可以将并发数看作是一个信号量,而对于并发数的控制,就是对于这个信号量的操作。我们可以通过P操作来减少并发数,通过V操作来增加并发数。

代码实现

我们可以通过一个 signal 变量来表示并发数,通过 pv 函数来实现P操作和V操作。

typescript 复制代码
function limit<Args extends readonly [], T>(limit: number, request: (...args: Args) => T): (...args: Args) => Promise<T> 
{

    const signal = limit;

    const tasks: Array<() => Promise<T>> = [];

    async function run() {
        if (signal > 0 && tasks.length > 0) {
            const task = tasks.shift();
            if (task) {
                // P操作
                signal--;
                await task();
                // V操作
                signal++;
                run();
            }
        }
    }

    return async (...args) => {
        return new Promise<T>((resolve, reject) => {
            tasks.push(async () => {
                try {
                    const result = await request(...args);
                    resolve(result);
                } catch (error) {
                    reject(error);
                }
            });
            run();
        });
    }
}

调用例子

typescript 复制代码
const request = limit(5, () => {
    return fetch('https://www.baidu.com');
});

for (let i = 0; i < 100; i++) {
    // 即使我们并发发起了100次请求,但是我们的并发数控制在了5个
    request();
}

如果你看懂了这段代码,那么恭喜你,你已经实现了 npm 上流行的一个包:p-limit 的核心逻辑。

但是,我们代码还有改进空间,比如我们对信号量 的操作过于简单,实际上,我们需要的其实不是一个简单的信号量,而是一个真实存在的资源

在这里例子中,我们的资源 其实就是一个可用的并发数 。但是在实际工作中,我们的资源可能是一个数据库连接,或者是一个文件句柄,或者是一个内存缓存。这些资源,都是有限的,我们需要对它们进行合理的管理。

资源池模式

资源池模式,是一种常见的并发控制模式。它的核心思想是:将资源抽象为一个池,然后对池中的资源进行管理

在我们的例子中,我们的资源就是一个并发数。我们可以将并发数抽象为一个池,然后对这个池进行管理。

资源池模式的核心,就是对资源的管理。我们可以通过一个 Pool 类来实现对资源的管理。

typescript 复制代码
class Pool<T extends object> {
    #resources: T[]
    async acquire(): Promise<T> {
        // 获取资源
    }
    release(resource: T) {
        // 释放资源
    }  
}

在这个类中,我们需要实现两个方法:acquirereleaseacquire 方法用于获取资源,release 方法用于释放资源。

release 方法

relase 方法比较简单,它只需要将资源放回池中即可。

typescript 复制代码
class Pool<T extends object> {
    #resources: T[]
    async acquire(): Promise<T> {
        // 获取资源
    }
    release(resource: T) {
        this.#resources.push(resource);
    }  
}

acquire 方法

acquire 方法比较复杂,它需要实现对资源的获取。在获取资源时,我们需要考虑两种情况:

  • 池中有空闲资源,直接返回
  • 池中没有空闲资源,等待资源释放后,再返回

我们需要引入一个通知机制,告知等待的线程,资源已经释放,可以返回了。

typescript 复制代码
class Pool<T> {
    #resources: T[]
    // releaseResolvers 是一个Promise的resolvers对象,包含promise和resolve方法
    #releaseResolvers: {
        promise: Promise<T>,
        resolve: (resource: T) => void,
    } | undefined;

    async acquire(): Promise<T> {
        // 获取资源
        if (this.#resources.length > 0) {
            return this.#resources.pop()!;
        } else {
            if (!this.#releaseResolvers) {
                this.#releaseResolvers = {
                    promise: new Promise<T>((resolve) => {
                        this.#releaseResolvers!.resolve = resolve;
                    }),
                    resolve: () => {},
                }
            }
            // 等待资源释放
            await this.#releaseResolvers.promise;
            // 重新获取资源
            return this.acquire();
        }
    }

    release(resource: T) {
        this.#resources.push(resource);
        if (this.#releaseResolvers) {
            // 通知等待的任务
            this.#releaseResolvers.resolve(resource);
            this.#releaseResolvers = undefined;
        }
    }

    constructor(resources: T[]) {
        this.#resources = resources;
    }

}

至此,我们就实现了一个资源池。我们可以通过这个资源池,来实现对于并发数的控制。

由于我们把很大一部分逻辑都放在了资源池中,所以我们的 limit 函数也变得非常简单。

typescript 复制代码
function limit<Args extends readonly [], T>(pool: Pool<T>, request: (...args: Args) => T): (...args: Args) => Promise<T> 
{
    return async (...args) => {
        const resource = await pool.acquire();
        try {
            const result = await request(...args);
            return result;
        } finally {
            pool.release(resource);
        }
    }
}

调用例子

typescript 复制代码
const pool = new Pool<number>([1, 2, 3, 4, 5]);

const request = limit(pool, () => {
    return fetch('https://www.baidu.com');
});

for (let i = 0; i < 100; i++) {
    // 即使我们并发发起了100次请求,但是我们的并发数控制在了5个
    request();
}

更多场景

这种模式虽然代码量相对于PV操作来说,多了很多,但是它的优势在于,它可以应用于更多的场景。

比如,我们需要做一个数据库连接池,我们就可以使用这种模式。

typescript 复制代码
// 我们简单改造一下 limit 函数,让资源可以传递进去
function limit<Args extends readonly [], T, R>(pool: Pool<T>, request: (this: T, ...args: Args) => R): (...args: Args) => Promise<R> 
{
    return async (...args) => {
        const resource = await pool.acquire();
        try {
            const result = await request.call(resource, ...args);
            return result;
        } finally {
            pool.release(resource);
        }
    }
}
typescript 复制代码
const pool = new Pool<Connection>([new Connection(), new Connection(), new Connection(), new Connection(), new Connection()]);

const selectTable = limit(pool, function() {
    // 我们甚至可以识别出this, 在this上调用方法
    // 或者传递中间过程的一些成员属性
    return this.query('select * from table');
});

另一个例子

我们可以使用这种模式,来实现一个Worker线程池。

typescript 复制代码
const pool = new Pool<Worker>([new Worker(), new Worker(), new Worker(), new Worker(), new Worker()]);

const run = limit(pool, function() {
    worker.postMessage('Hello');
});

总结

今天,我们用一个经典的并发问题,引用了PV操作和资源池模式。这两种模式,都是解决并发问题的常见模式,也是面试中的常客。希望大家能够掌握这两种模式,也希望大家能够在面试中,优雅的回答并发问题。

@cat5th/pool.js

上述代码的内容,大部分来源于我工作的点滴,所以我将其封装成了一个npm包:@cat5th/pool.js

希望大家能够喜欢。如果有什么问题,欢迎提issue。感谢各位不吝赐教。顺手点个star吧。

相关推荐
懒大王爱吃狼41 分钟前
Python教程:python枚举类定义和使用
开发语言·前端·javascript·python·python基础·python编程·python书籍
待磨的钝刨2 小时前
【格式化查看JSON文件】coco的json文件内容都在一行如何按照json格式查看
开发语言·javascript·json
逐·風5 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#
Devil枫5 小时前
Vue 3 单元测试与E2E测试
前端·vue.js·单元测试
尚梦6 小时前
uni-app 封装刘海状态栏(适用小程序, h5, 头条小程序)
前端·小程序·uni-app
GIS程序媛—椰子6 小时前
【Vue 全家桶】6、vue-router 路由(更新中)
前端·vue.js
前端青山7 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
毕业设计制作和分享7 小时前
ssm《数据库系统原理》课程平台的设计与实现+vue
前端·数据库·vue.js·oracle·mybatis
从兄8 小时前
vue 使用docx-preview 预览替换文档内的特定变量
javascript·vue.js·ecmascript
清灵xmf9 小时前
在 Vue 中实现与优化轮询技术
前端·javascript·vue·轮询