手写并发控制、消息队列(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

相关推荐
m0_7482338816 分钟前
黑马程序员JavaWeb开发教程(前端部分) ---笔记分享
前端·笔记
温轻舟22 分钟前
前端开发 -- 自定义鼠标指针样式
开发语言·前端·javascript·css·html·温轻舟
冰镇屎壳郎1 小时前
前端安全 常见的攻击类型及防御措施
前端·安全·前端安全
2401_857617621 小时前
“无缝购物体验”:跨平台网上购物商城的设计与实现
java·开发语言·前端·安全·架构·php
2401_857439691 小时前
智慧社区电商系统:提升用户体验的界面设计
前端·javascript·php·ux
我是高手高手高高手1 小时前
ThinkPHP8多应用配置及不同域名访问不同应用的配置
linux·服务器·前端·php
小李小李不讲道理1 小时前
行动+思考 | 2024年度总结
前端·程序员·年终总结
uhakadotcom1 小时前
代码人生-精选文章周刊
前端·后端·github
csdnLN2 小时前
$.ajax() 对应事件done() 、fail()、always() 的用法
前端·javascript·ajax
甜味橘阳2 小时前
echarts地图可视化展示
前端·javascript·echarts