【前端面试小册】JS-第20节:实现并发限制(滴滴、头条面试题)

第20节:实现并发限制(滴滴、头条面试题)

一、题目描述

1.1 需求分析

实现一个并发限制函数 concurrenceLimits(tasks, limit)

参数

  • tasks:任务队列(请求列表),每个任务是一个返回 Promise 的函数
  • limit:每次同时并发最多允许的请求数目

要求

  1. 同时发出 n 个请求(n <= limit
  2. 只要其中一个完成请求,立马又加入一个请求
  3. 让请求队列始终保持 n 个请求,直到没有任务再可以加入
  4. 返回的结果必须按照任务队列的顺序返回,返回结果类似于 Promise.all()

1.2 示例说明

concurrenceLimits([req1, req2, req3], 2)

执行过程

  1. 初始阶段:同时发出请求 req1req2(并发数为 2)
  2. req2 完成req1 还没有完成,因为并发最多可以有两个请求,所以这个时候 req3 也发出请求
  3. 结果返回:虽然 req2 最先返回,但是返回的结果必须按照请求队列顺序返回,即结果必须是:[res1, res2, res3]

执行流程图

alt

二、实现思路

2.1 核心思想

  1. 任务队列管理:维护一个正在执行的任务队列
  2. 并发控制:使用 Promise.race 等待任意一个任务完成
  3. 顺序保证:使用结果数组按顺序存储结果
  4. 动态调度:任务完成后立即启动新任务

2.2 关键点

  • 任务队列:存储正在执行的 Promise
  • 结果队列:按顺序存储所有任务的 Promise
  • Promise.race:等待任意一个任务完成
  • 动态调度:任务完成后继续执行下一个任务

三、完整实现

3.1 基础版本

function concurrenceLimits(tasks, limit) {
    let count = 0;  // 已完成任务计数
    
    // 结果队列:按顺序存储所有任务的 Promise
    let result = [];
    
    // 任务队列:存储正在执行的 Promise
    let queue = [];
    
    let len = tasks.length;
    
    function run() {
        // 执行完所有任务后返回
        if (count === len) {
            return Promise.resolve();
        }
        
        // 获取当前任务
        let task = tasks[count++];
        
        // 每个任务都以 Promise 返回,防止传入的任务非 Promise
        let taskPromise = Promise.resolve(task());
        
        // 将当前正在执行的任务放入【结果队列】
        result.push(taskPromise);
        
        // 当前任务执行完以后,从【任务队列中】删除该任务
        // 这样才能继续往任务队列新加任务
        let _taskPromise = taskPromise.then(() => {
            queue.splice(queue.indexOf(_taskPromise), 1);
        });
        
        // 当前任务放入【任务队列】中
        queue.push(_taskPromise);
        
        let p = Promise.resolve();
        
        // 当任务队列长度达到并发限制数目,暂停往任务队列添加任务
        if (queue.length >= limit) {
            // race 表明只要完成一个任务后,p 就可以执行回调
            // 执行下面的 p.then(() => run()),继续添加任务
            p = Promise.race(queue);
        }
        
        // 注意这里:
        // - 任务队列小于并发限制,跳过 queue.length >= limit,直接执行 p.then 的回调 run 添加任务队列
        // - 任务队列达到并发限制数目后,必须等 Promise.race(queue) 执行完一个任务后,执行 p.then 的回调 run 添加任务队列
        return p.then(() => run());
    }
    
    // 所有任务执行完以后回调,按顺序返回执行结果
    return run().then(() => Promise.all(result));
}

3.2 执行流程详解

graph TD
    A[调用 concurrenceLimits] --> B[初始化变量]
    B --> C[调用 run 函数]
    C --> D{count === len?}
    D -->|是| E[返回 Promise.resolve]
    D -->|否| F[获取任务 tasks count++]
    F --> G[执行任务 task]
    G --> H[将 Promise 加入 result]
    H --> I[将 Promise 加入 queue]
    I --> J{queue.length >= limit?}
    J -->|是| K[Promise.race queue]
    J -->|否| L[Promise.resolve]
    K --> M[等待任意任务完成]
    L --> M
    M --> N[执行 run 继续添加任务]
    N --> D
    E --> O[Promise.all result]
    O --> P[按顺序返回结果]

3.3 关键代码解析

3.3.1 任务执行

let task = tasks[count++];
let taskPromise = Promise.resolve(task());

作用

  • 获取当前任务并执行
  • 使用 Promise.resolve 确保返回 Promise

3.3.2 结果队列

result.push(taskPromise);

作用

  • 按顺序存储所有任务的 Promise
  • 最后使用 Promise.all(result) 按顺序返回结果

3.3.3 任务队列管理

let _taskPromise = taskPromise.then(() => {
    queue.splice(queue.indexOf(_taskPromise), 1);
});
queue.push(_taskPromise);

作用

  • 任务完成后从队列中删除
  • 保持队列中只有正在执行的任务

3.3.4 并发控制

if (queue.length >= limit) {
    p = Promise.race(queue);
}
return p.then(() => run());

作用

  • 当队列达到并发限制时,等待任意一个任务完成
  • 任务完成后继续执行 run(),添加新任务

四、测试用例

4.1 基础测试

// 模拟请求
function simulateRequest(response, time) {
    const r = `耗时hash_${String(Math.random()).slice(2, 5)}`;
    console.time(r);
    return () => {
        console.log(`开始请求${response}`);
        return new Promise(resolve => setTimeout(() => {
            console.log(`请求${response}: 完成`);
            console.timeEnd(r);
            resolve(response);
        }, time, response));
    };
}

// 测试 DEMO
concurrenceLimits([
    simulateRequest('a', 5000),
    simulateRequest('b', 2000),
    simulateRequest('c', 1000)],
    2).then(res => {
    console.log('result', res);
});

// 控制台输出:
// 开始请求a
// 开始请求b
// 请求b: 完成
// 耗时hash_039: 2.011s
// 开始请求c
// 请求c: 完成
// 耗时hash_114: 3.015s
// 请求a: 完成
// 耗时hash_241: 5.012s
// result [ 'a', 'b', 'c' ]

注意:虽然 ba 先返回,但是最后的结果仍然是按照队列加入顺序来。

4.2 执行时间线

时间轴:
0s    ──> 开始请求 a (5s)
0s    ──> 开始请求 b (2s)
2s    ──> 请求 b 完成,开始请求 c (1s)
3s    ──> 请求 c 完成
5s    ──> 请求 a 完成
      ──> 返回结果 [a, b, c]

五、优化版本

5.1 代码优化

function concurrenceLimits(tasks, limit) {
    if (!Array.isArray(tasks) || tasks.length === 0) {
        return Promise.resolve([]);
    }
    
    if (typeof limit !== 'number' || limit <= 0) {
        throw new TypeError('limit must be a positive number');
    }
    
    const results = [];
    const executing = [];
    let index = 0;
    
    function run() {
        // 所有任务已完成
        if (index >= tasks.length) {
            return Promise.resolve();
        }
        
        // 执行任务
        const task = tasks[index++];
        const promise = Promise.resolve(task())
            .then(result => {
                // 任务完成,从执行队列中移除
                executing.splice(executing.indexOf(promise), 1);
                return result;
            });
        
        results.push(promise);
        executing.push(promise);
        
        // 如果达到并发限制,等待任意一个任务完成
        let racePromise = Promise.resolve();
        if (executing.length >= limit) {
            racePromise = Promise.race(executing);
        }
        
        // 继续执行下一个任务
        return racePromise.then(() => run());
    }
    
    return run().then(() => Promise.all(results));
}

5.2 支持错误处理

function concurrenceLimits(tasks, limit) {
    // ... 前面的代码 ...
    
    function run() {
        if (index >= tasks.length) {
            return Promise.resolve();
        }
        
        const task = tasks[index++];
        const promise = Promise.resolve(task())
            .then(result => {
                executing.splice(executing.indexOf(promise), 1);
                return result;
            })
            .catch(error => {
                executing.splice(executing.indexOf(promise), 1);
                throw error;  // 重新抛出错误
            });
        
        results.push(promise);
        executing.push(promise);
        
        let racePromise = Promise.resolve();
        if (executing.length >= limit) {
            racePromise = Promise.race(executing);
        }
        
        return racePromise.then(() => run());
    }
    
    return run().then(() => Promise.all(results));
}

5.3 支持进度回调

function concurrenceLimits(tasks, limit, onProgress) {
    // ... 前面的代码 ...
    
    function run() {
        if (index >= tasks.length) {
            return Promise.resolve();
        }
        
        const task = tasks[index++];
        const promise = Promise.resolve(task())
            .then(result => {
                executing.splice(executing.indexOf(promise), 1);
                if (onProgress) {
                    onProgress(results.length, tasks.length);
                }
                return result;
            });
        
        // ... 其余代码 ...
    }
    
    return run().then(() => Promise.all(results));
}

六、实际应用场景

6.1 批量请求 API

// 批量获取用户信息
const userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const tasks = userIds.map(id => () => 
    fetch(`/api/users/${id}`).then(res => res.json())
);

concurrenceLimits(tasks, 3).then(users => {
    console.log('所有用户信息:', users);
});

6.2 文件上传

// 批量上传文件
const files = [file1, file2, file3, file4, file5];
const tasks = files.map(file => () => 
    uploadFile(file)
);

concurrenceLimits(tasks, 2).then(results => {
    console.log('上传结果:', results);
});

6.3 数据爬取

// 爬取多个页面
const urls = ['url1', 'url2', 'url3', 'url4'];
const tasks = urls.map(url => () => 
    fetch(url).then(res => res.text())
);

concurrenceLimits(tasks, 2).then(contents => {
    console.log('爬取结果:', contents);
});

七、性能分析

7.1 时间复杂度

  • 时间复杂度:O(n),其中 n 是任务数量
  • 空间复杂度:O(limit),队列最多存储 limit 个任务

7.2 优化建议

  1. 合理设置并发数:根据服务器性能和网络状况设置
  2. 错误处理:添加错误处理,避免单个任务失败影响整体
  3. 进度反馈:添加进度回调,提升用户体验

八、面试要点总结

核心知识点

  1. 并发控制:使用 Promise.race 控制并发数量
  2. 顺序保证:使用结果数组按顺序存储结果
  3. 动态调度:任务完成后立即启动新任务
  4. Promise 链:使用 Promise 链实现递归调度

常见面试题

Q1: 如何实现并发限制?

答:维护一个执行队列,当队列达到并发限制时,使用 Promise.race 等待任意一个任务完成,然后继续添加新任务。使用结果数组按顺序存储结果,最后使用 Promise.all 按顺序返回。

Q2: 如何保证结果顺序?

答:使用结果数组按顺序存储所有任务的 Promise,最后使用 Promise.all 按顺序返回结果。

Q3: Promise.race 的作用?

答:Promise.race 返回第一个完成的 Promise,用于等待任意一个任务完成,实现并发控制。

实战建议

  • ✅ 理解并发控制的原理
  • ✅ 注意结果顺序的保证
  • ✅ 添加错误处理和边界情况处理
  • ✅ 根据实际场景调整并发数
#前端面试##前端#
前端面试小册 文章被收录于专栏

每天更新3-4节,持续更新中... 目标:50天学完,上岸银行总行!

全部评论

相关推荐

滴滴一面(45min)1.&nbsp;flex=1是哪些属性的简写2.&nbsp;假如父盒子是&nbsp;100,两个子盒子分别设置&nbsp;flex&nbsp;等于&nbsp;0.1,flex&nbsp;0.2,它的宽度怎么分配呢3.&nbsp;CSS&nbsp;如何画三角形4.&nbsp;移动端的适配,有各种宽高不同的手机嘛,想要前端的页面写一套在各个手机型号上都能有良好的运行的程序,有什么设计方案吗5.&nbsp;rem怎么计算的,小程序的屏幕适配用什么属性6.&nbsp;了解开发这个屏幕比例适配用什么第三方库吗7.&nbsp;代码在各个浏览器上,因为&nbsp;API&nbsp;的兼容性可能不同,有没有了解过前端的一些兼容的方案?8.&nbsp;Babel&nbsp;Polyfill&nbsp;解决了什么问题(Promise&nbsp;、&nbsp;fetch&nbsp;、&nbsp;Array.includes)9.&nbsp;js作用域链的查找规则10.&nbsp;代码阅读题,函数全局变量和局部变量输出11.&nbsp;手撕1:手写&nbsp;promise&nbsp;串行不用&nbsp;await12.&nbsp;手撕2:给前序遍历和中序遍历的二叉树数组,构造二叉树并返回根节点13.&nbsp;手撕3:最长不重复子串14.&nbsp;前端的部署流程15.&nbsp;前端打包的产物是啥16.&nbsp;前端缓存方式(强缓存,协商缓存),CDN的原理,调度方式,&nbsp;js、css&nbsp;文件是否可以放入CDN17.&nbsp;单页面应用的路由实现原理是啥?它和普通多页面应用的路由有啥区别?18.&nbsp;路由的两种模式(history和hash)的区别19.&nbsp;webpack主要是干啥的?解决什么问题的?20.&nbsp;为啥要打包?&nbsp;webpack&nbsp;打包的产物,为什么&nbsp;js&nbsp;有增加一些哈希值啥之类的,了解吗?21.&nbsp;为什么index.html它没有一个什么哈希标识呢?22.&nbsp;Vue中的diff&nbsp;算法的头头比和头尾比的区别23.&nbsp;Vue2和Vue3对于数组和对象的响应式区别24.&nbsp;Vue2&nbsp;技术上能劫持数组索引,但为什么不做?(性能成本极高,,初始化时要遍历&nbsp;每一个索引&nbsp;加getter/setter&nbsp;,会导致内存爆炸)滴滴二面(50min)实习相关(白屏检测怎么处理JS报错的问题)1.&nbsp;TCP&nbsp;和&nbsp;UDP的区别2.&nbsp;WebSocket&nbsp;是什么,它是基于&nbsp;TCP&nbsp;还是基于&nbsp;UDP3.&nbsp;介绍React&nbsp;中虚拟&nbsp;DOM4.&nbsp;手撕:士兵身高从低到高的冒泡排序滴滴三面(30min)实习+项目
LoyAngel:45分钟手撕三道题😱😱给佬👻了
查看28道真题和解析
点赞 评论 收藏
分享
1.自我介绍2.专业主要学什么(信计专业)3.为什么要学前端4.数学建模竞赛主要是承担什么角色,比起一等奖差在哪里(这个是因为获奖经历写了)5.在浏览器中输入地址回车后会发生什么(这里的话还延伸了渲染的具体过程)6.页面渲染过程中,遇到script标签会发生什么7.有什么方法可以避免script阻塞渲染(这里只回答了把他放到body底部,面试官其实想问的是异步编程)8.脚本加载失败,怎么能让他正常渲染出来(没答)9.讲讲less/sass进行数据预处理,对比原生css的优点,为什么要用它,不直接用原生css,有什么特性,带来了哪些好处(答了可以把伪类放在一起,开发方便)(这里面试官提到了postcss后处理)10.js有哪些异步编程的方法,语法糖对比promise的优势11.await到底在等什么,后面跟的是什么东西(语法糖➕函数是promise对象)12.ts对比js有什么优势,核心优势(面试官说类型检查,开发的时候会报错及时发现,还有代码提示)13.vue的响应式原理(这里听成了响应式布局的原理,后面面试官指出来之后找补了点内容,发布+订阅者模式,get依赖收集,set发布)14.项目中的图片懒加载怎么实现(答了observation&nbsp;api,面试官让回答原生代码,也就是滚动事件监听)反问环节:1.部门的产品(用户体验平台),规模()20多个人,技术栈(vue和react都有)2.根据面试表现,后续有什么改进的方向(基础知识还要加强,也多拓展相关知识点和关联性)
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务