ocev
ocev
是一个事件库, 本文主要介绍下 ocev
的一些应用案例, 如何简单优雅的实现一些常用模块
觉得有用的话,请帮我点个 ⭐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
}
}