并发控制:让你的异步任务排队不再“打架”!🚦

96 阅读6分钟

前言

在前端开发的世界里,异步任务就像一群急性子的“选手”,总想一拥而上,争先恐后地完成比赛。但如果不加以控制,网络请求、文件上传、数据处理等任务就会像春运抢票一样,场面失控,资源争抢,甚至导致性能瓶颈和用户体验下降。于是,并发控制机制应运而生,成为前端工程师的“调度大师”。


为什么需要并发控制?

想象一下,如果你让所有异步任务同时发起请求,服务器可能会被“挤爆”,浏览器也会卡成PPT。并发控制的核心目的就是:限制同时进行的任务数量,让资源分配更合理,避免“拥堵”和“踩踏”。

  • 场景举例
    • 批量图片上传,限制同时上传的数量,防止带宽被瞬间占满。
    • 爬虫抓取网页,控制并发数,避免被目标网站封禁。
    • 批量数据处理,合理分配CPU资源,提升整体性能。

并发控制的原理

并发控制机制本质上就是一个“任务调度器”,它负责:

  1. 维护一个任务队列,所有待执行的任务都排队等候。
  2. 设定最大并发数,只允许有限数量的任务同时进行。
  3. 每当有任务完成,立即从队列中取下一个任务补位,保证“流水线”不断。

就像电影院检票口,只允许有限人数同时进入,后面的人只能乖乖排队。


代码实现大揭秘

下面我们结合实际代码,详细拆解一个并发控制类的实现过程。

1. 任务模拟器 ajax

首先,我们用一个 ajax 函数模拟异步任务:

// 模拟一个异步任务,传入不同的耗时参数 time
function ajax(time) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            // 如果任务耗时超过 5000ms,则判定为失败
            if (time > 5000) {
                reject(time)
            }
            // 否则判定为成功
            resolve(time)
        }, time) // 任务实际耗时由 time 决定
    })
}

讲解:

  • 这个函数就是我们的“任务工厂”,每次调用都会返回一个 Promise。
  • setTimeout 模拟异步操作,time 越大任务越慢。
  • 超过 5 秒的任务会被 reject,模拟失败场景。

2. 并发控制类 Limit

核心来了!我们用一个 Limit 类来实现并发控制:

// 并发控制类,负责调度任务队列
class Limit {
    constructor(parallCount = 2) {
        this.parallCount = parallCount // 最大并发数,默认 2
        this.tasks = [] // 任务队列,存储所有待执行任务
        this.runningCount = 0 // 当前正在运行的任务数量
    }
    // 添加新任务,返回一个 Promise 以便外部获取结果
    add(task) {
        return new Promise((resolve, reject) => {
            // 将任务及其对应的 resolve/reject 存入队列
            this.tasks.push({
                task, // 任务函数,需返回 Promise
                resolve, // 任务成功时调用
                reject,  // 任务失败时调用
            })
            // 每次添加任务都尝试调度执行
            this._run()
        })
    }
    // 内部调度器,负责按最大并发数执行任务
    _run() {
        // 如果当前运行任务数未达到最大并发,且队列有待执行任务
        if (this.runningCount < this.parallCount && this.tasks.length) {
            // 从队列取出一个任务
            const {task, resolve, reject }= this.tasks.shift()
            this.runningCount++ // 当前运行任务数加一
            // 执行任务(需返回 Promise)
            task().then((res) => {
                resolve(res) // 任务成功,通知外部
                this.runningCount-- // 运行任务数减一
                this._run() // 递归调度下一个任务
            }).catch((res) => {
                this.runningCount-- // 运行任务数减一
                reject(res) // 任务失败,通知外部
                this._run() // 递归调度下一个任务
            })
        }
    }
}

详细讲解:

  • parallCount 控制最大并发数,灵活应对不同场景。
  • tasks 是一个先进先出的队列,保证任务顺序执行。
  • runningCount 实时记录当前正在执行的任务数量。
  • add 方法将任务包装成对象,方便后续调度和结果回调。
  • _run 方法是调度核心,每次有任务完成都会递归调用,保证队列持续流动。
  • 这种设计可以防止任务“堆积”,让资源利用率最大化。

3. 添加任务并观察效果

我们用 addTask 函数批量添加任务,并观察并发控制的效果:

// 封装添加任务的函数,方便批量测试
function addTask(time, name) {
    limit
    .add(() => ajax(time)) // 添加任务,传入异步函数
    .then((res) => {
        // 任务成功时输出结果
        console.log(`任务${name}完成`, res);
    })
    .catch((res) => {
        // 任务失败时输出结果
        console.log(`任务${name}失败`, res);
    })
}

// 批量添加不同耗时的任务
addTask(10000, '1') // 10秒,必失败
addTask(4000, '2')  // 4秒,成功
addTask(2000, '3')  // 2秒,成功
addTask(5000, '4')  // 5秒,成功
addTask(1000, '5')  // 1秒,成功
addTask(7000, '6')  // 7秒,必失败

讲解:

  • 每个任务耗时不同,模拟真实场景下的异步请求。
  • 并发数设置为 2,意味着最多同时跑 2 个任务。
  • 任务完成或失败后,自动补位下一个任务,队列像流水线一样高效运转。

4. 运行流程可视化与分析

假如你用表格或时序图展示,任务的执行顺序会像这样:

时间(ms)任务1任务2任务3任务4任务5任务6
0开始开始等待等待等待等待
4000运行完成开始等待等待等待
5000运行完成开始等待等待
10000完成完成开始等待
...完成开始
...完成

分析补充:

  • 任务队列始终保持“满载”,只要有空位就自动补上新任务。
  • 这种机制极大提升了资源利用率,避免了“空闲浪费”。
  • 失败的任务不会影响队列流动,健壮性更强。

代码优化与扩展建议

  1. 支持动态调整并发数:可以增加方法动态修改 parallCount,适应不同场景。
  2. 任务优先级调度:可扩展为优先队列,让高优先级任务优先执行。
  3. 失败重试机制:为失败的任务增加重试次数,提高健壮性。
  4. 任务取消功能:支持任务取消,提升灵活性。
  5. 进度回调与事件监听:可增加事件机制,实时反馈任务进度。
  6. Promise.all 并发对比:传统的 Promise.all 会一次性并发所有任务,容易造成资源拥堵,而并发控制机制则能“限流”,更适合大规模异步场景。

实战场景应用

  • 批量文件上传:限制同时上传的文件数量,防止网络拥堵。
  • 爬虫抓取:控制并发数,避免被目标网站封禁。
  • 数据处理:合理分配 CPU 资源,提升整体性能。
  • 前端渲染:异步渲染大数据列表,避免页面卡顿。
  • API 请求限流:防止接口被频繁调用导致封禁。

总结

并发控制机制是前端开发中的“交通警察”,让异步任务有序排队,资源分配更合理,性能更高效。无论是面试还是实际项目,掌握并发控制都是前端工程师的必备技能。希望本文能帮你轻松理解并发控制的原理和实现,写出更高效、更优雅的前端代码!