手写并发控制、消息队列(RingBuffer),ocev.js 应用案例

ocev

ocev 是一个事件库, 本文主要介绍下 ocev 的一些应用案例, 如何简单优雅的实现一些常用模块

github 地址 ,文档地址介绍

觉得有用的话,请帮我点个 ⭐star

定义类型

定义 Task 类型,后面的案例都会用到

typescript 复制代码
type Task<Context = void> = (context: Context) => Promise<void> | void

并发控制

并发控制是一种很常见的需求,用于控制异步函数最大执行数量,比如限制网络请求数量

先定义每个任务 都是一个异步的函数 Task<void>,也就是 type Task = () => Promise<void>,下面是几种实现方式

方法一

定义每个任务都是一个异步的函数 Task<void>

重点在于每个 Task 运行前和运行后都会对 currentRunningCount 进行加减

每当有新任务被创建,或者旧任务结束的时候,都去检查一下队列中是否有任务可以接着被运行

typescript 复制代码
class ConcurrencyLimit {
  private currentRunningCount = 0

  private queue: Task<void>[]

  private capacity

  constructor(capacity: number) {
    this.queue = new Array(capacity)
    this.capacity = capacity
  }
  // 创建一个任务
  createTask = (task: Task<void>) => {
    const taskWrapper = async () => {
      this.currentRunningCount += 1
      try {
        await task?.()
      } finally {
        this.currentRunningCount -= 1
        this.checkAndRun() // 任务结束的时候检查
      }
    }

    this.queue.push(taskWrapper)
    this.checkAndRun() // 新增任务的时候检查
  }
  // 每当有新任务或者旧任务结束的时候,都去检查下是否有新的任务可以被运行
  private checkAndRun = () => {
    if (this.currentRunningCount >= this.capacity) {
      return
    }

    if (!this.queue.length) return

    const task = this.queue.shift()
    task?.()
  }
}

动态的设置容量 capacity

typescript 复制代码
class ConcurrencyLimit {
  // ........
  // 更新容量
  setCapacity = (capacity: number) => {
    this.capacity = capacity
    // 检查一下,假设 capacity 变大,可以运行新的任务
    this.checkAndRun()
  }
  
  // ........
}

方法二

上面的代码处处都要调用 checkAndRun,可以换一种思路来处理

使用 ocev.js 进行改造

使用 ocev.js 里面的 SyncEvent 处理, SyncEvent 文档

重点在于把每一个操作都定义成事件 ,然后将所有事件汇集起来用一个协程处理

typescript 复制代码
class ConcurrencyLimit {
  private currentRunningCount = 0

  private queue: Task<void>[]

  private capacity

  // 定义一个事件中心
  private ev = SyncEvent.new<{
    capacityChange: VoidFunction
    newTask: VoidFunction
    endTask: VoidFunction
  }>()

  constructor(capacity: number) {
    this.queue = new Array(capacity)
    this.capacity = capacity
  }

  setCapacity = (capacity: number) => {
    this.capacity = capacity
    // checkAndRun 转换成事件
    this.ev.emit("capacityChange")
  }

  createTask = (task: Task<void>) => {
    const taskWrapper = async () => {
      this.currentRunningCount += 1
      try {
        await task?.()
      } finally {
        this.currentRunningCount -= 1
        // checkAndRun 转换成事件
        this.ev.emit("endTask")
      }
    }

    this.queue.push(taskWrapper)
    // checkAndRun 转换成事件 
    this.ev.emit("newTask")
  }
}

然后我们启动一个协程,用于管理任务

typescript 复制代码
class ConcurrencyLimit {
  // ....
  constructor(capacity: number) {
    this.queue = new Array(capacity)
    this.capacity = capacity
    this.process()
  }
  // .......
  private process = async () => {
    // 死循环,一个一直运行下去的协程
    for (;;) {
      while (this.currentRunningCount >= this.capacity || !this.queue.length) {
        // 如果上面的条件不满足,就一直等待这些事件中的任意一个触发
        await this.ev.waitUtilRace(["endTask", "newTask", "capacityChange"])
      }
    
      const task = this.queue.shift() // task 肯定是有的
      task?.()
    }
  }
  // .......
}

通过事件,我们可以更加优雅的实现代码

方法三,创建 N 个协程处理,不支持动态设置 capacity

typescript 复制代码
class ConcurrencyLimit {
  private queue: Task<void>[] = []

  private capacity

  private ev = SyncEvent.new<{ newTask: VoidFunction }>()

  constructor(capacity: number) {
    this.capacity = capacity
    
    // 创建  capacity 个协程处理任务
    for (let i = 0; i < this.capacity; i++) {
      this.process()
    }
  }

  createTask = (task: Task<void>) => {
    this.queue.push(task)
    this.ev.emit("newTask") // 每次唤醒所有的协程来检查
  }
  // 协程,有任务就处理,处理完一个就接下一个,没任务就等待
  private process = async () => {
    for (;;) {
      while (!this.queue.length) {
        await this.ev.waitUtil("newTask")  // 队列为空的时候,等待有新的任务
      }
      const task = this.queue.shift()
      try {
        await task?.()
      } catch {
        // nothing to do
      }
    }
  }
}

MessageQueue

假设我们有一系列任务需要运行,但是不在当前执行,我们不关心这个任务什么时候被运行,只要保证任务顺序的执行就行了,这个时候可以使用一个 MessageQueue 来实现

如果你能理解并使用ocev.js, 那么你就能很简单的实现了

typescript 复制代码
class MessageQueue<Context> {
  private queue: Task<Context>[] = []

  private ev = SyncEvent.new<{ newTask: VoidFunction }>()

  createTask = (task: Task<Context>) => {
    this.queue.push(task)
    this.ev.emit("newTask")
  }

  process = async (context: Context) => {
    for (;;) {
      while (!this.queue.length) {
        await this.ev.waitUtil("newTask")
      }
      const task = this.queue.shift()
      await task?.(context)
    }
  }
}

使用的时候

typescript 复制代码
// 创建 
const queue = new MessageQueue<{ doSomething: () => Promise<void> }>()

生产者

typescript 复制代码
queue.createTask(async (context) => {
  await context.doSomething()
})

消费者

typescript 复制代码
// 不知道什么时候才能初始化完成
const context = await init()
queue.process(context)

RingBuffer

最后我们来看一下 RingBuffer, RingBuffer底层是一个环形数组,出队和入队的复杂度都是 O(1), 但是容量要保持不变, 通过两个指针标识读和写的位置

关于RingBuffer 的具体介绍请看 wiki, 上面写的非常详细,RingBuffer 常用于音视频处理当中

先定义 RingBuffer

typescript 复制代码
interface IRingBuffer<T> {
  enqueue: (value: T) => Promise<void> // 如果队列已满,enqueue 会阻塞直到推入成功
  dequeue: () => Promise<T> // 如果队列为空,阻塞直到有值可以被推出
}

根据上面的接口定义,很容易得到下面的代码,重点在于两个等待

typescript 复制代码
class RingBuffer<T> implements IRingBuffer<T> {
  private queue: T[]

  private capacity

  private writeIndex = 0
  private readIndex = 0

  get isEmpty() {
    return this.writeIndex === this.readIndex
  }

  get isFull() {
    return this.writeIndex - this.readIndex >= this.capacity
  }

  constructor(capacity: number) {
    this.queue = new Array(capacity)
    this.capacity = capacity
  }

  enqueue = async (value: T) => {
    while (this.isFull) {
      // 等待队列有值被推出
    }
    this.queue[this.writeIndex % this.queue.length] = value
    this.writeIndex += 1
  }

  dequeue = async () => {
    while (this.isEmpty) {
      // 等待队列有值写入
    }
    const value = this.queue[this.readIndex % this.queue.length]
    this.readIndex += 1
    return value
  }
}

根据上面的介绍,最后的代码如下

typescript 复制代码
class RingBuffer<T> implements IRingBuffer<T> {
  private queue: T[]

  private capacity

  private writeIndex = 0
  private readIndex = 0

  private ev = SyncEvent.new<{ write: VoidFunction; read: VoidFunction }>()

  get isEmpty() {
    return this.writeIndex === this.readIndex
  }

  get isFull() {
    return this.writeIndex - this.readIndex >= this.capacity
  }

  constructor(capacity: number) {
    this.queue = new Array(capacity)
    this.capacity = capacity
  }

  enqueue = async (value: T) => {
    while (this.isFull) {
      await this.ev.waitUtil("read") // 等待队列有值被推出
    }
    this.queue[this.writeIndex % this.queue.length] = value
    this.writeIndex += 1
    this.ev.emit("write") // 触发写的事件
  }

  dequeue = async () => {
    while (this.isEmpty) {
      await this.ev.waitUtil("write") // 等待队列有值写入
    }
    const value = this.queue[this.readIndex % this.queue.length]
    this.readIndex += 1
    this.ev.emit("read") // 触发读的事件
    return value
  }
}

总结

上面就是 ocev.js 的一些应用案例, 如果对您有帮助,请帮我点个 ⭐star,谢谢了 , github

相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax