使用async/await在JS/TS中的异步有界队列

Hug*_*ira 11 javascript asynchronous async-await typescript

我试图绕过头async/await,我有以下代码:

class AsyncQueue<T> {
    queue = Array<T>()
    maxSize = 1

    async enqueue(x: T) {
        if (this.queue.length > this.maxSize) {
            // Block until available
        }

        this.queue.unshift(x)
    }

    async dequeue() {
        if (this.queue.length == 0) {
            // Block until available
        }

        return this.queue.pop()!
    }
}

async function produce<T>(q: AsyncQueue, x: T) {
    await q.enqueue(x)
}

async function consume<T>(q: AsyncQueue): T {
    return await q.dequeue()
}

// Expecting 3 4 in the console
(async () => {
    const q = new AsyncQueue<number>()
    consume(q).then(console.log)
    consume(q).then(console.log)
    produce(q, 3)
    produce(q, 4)
    consume(q).then(console.log)
    consume(q).then(console.log)
})()
Run Code Online (Sandbox Code Playgroud)

当然,我的问题在于代码中的"阻塞直到可用"部分.我希望能够"停止"执行直到发生某些事情(例如,出列暂停直到出现入队,反之亦然,考虑到可用空间).我觉得我可能需要使用协同程序,但我真的想确保我不会错过任何async/await魔法.

Hug*_*ira 6

最后,经过相当大的努力,受@Titian答案的启发,我想我解决了这个问题.代码中充满了调试消息,但它可能有助于控制流程的教学目的:

class AsyncSemaphore {
    private promises = Array<() => void>()

    constructor(private permits: number) {}

    signal() {
        this.permits += 1
        if (this.promises.length > 0) this.promises.pop()!()
    }

    async wait() {
        this.permits -= 1
        if (this.permits < 0 || this.promises.length > 0)
            await new Promise(r => this.promises.unshift(r))
    }
}
Run Code Online (Sandbox Code Playgroud)

更新:这是一个使用的干净版本AsyncSemaphore,它真正封装了通常使用并发原语完成的方式,但适用于异步CPS-单线程事件循环™风格的JavaScript async/await.您可以看到逻辑AsyncQueue变得更直观,并且通过Promises的双重同步被委托给两个信号量:

class AsyncQueue<T> {
    waitingEnqueue = new Array<() => void>()
    waitingDequeue = new Array<() => void>()
    enqueuePointer = 0
    dequeuePointer = 0
    queue = Array<T>()
    maxSize = 1
    trace = 0

    async enqueue(x: T) {
        this.trace += 1
        const localTrace = this.trace

        if ((this.queue.length + 1) > this.maxSize || this.waitingDequeue.length > 0) {
            console.debug(`[${localTrace}] Producer Waiting`)
            this.dequeuePointer += 1
            await new Promise(r => this.waitingDequeue.unshift(r))
            this.waitingDequeue.pop()
            console.debug(`[${localTrace}] Producer Ready`)
        }

        this.queue.unshift(x)
        console.debug(`[${localTrace}] Enqueueing ${x} Queue is now [${this.queue.join(', ')}]`)

        if (this.enqueuePointer > 0) {
            console.debug(`[${localTrace}] Notify Consumer`)
            this.waitingEnqueue[this.enqueuePointer-1]()
            this.enqueuePointer -= 1
        }
    }

    async dequeue() {
        this.trace += 1
        const localTrace = this.trace

        console.debug(`[${localTrace}] Queue length before pop: ${this.queue.length}`)

        if (this.queue.length == 0 || this.waitingEnqueue.length > 0) {
            console.debug(`[${localTrace}] Consumer Waiting`)
            this.enqueuePointer += 1
            await new Promise(r => this.waitingEnqueue.unshift(r))
            this.waitingEnqueue.pop()
            console.debug(`[${localTrace}] Consumer Ready`)
        }

        const x = this.queue.pop()!
        console.debug(`[${localTrace}] Queue length after pop: ${this.queue.length} Popping ${x}`)

        if (this.dequeuePointer > 0) {
            console.debug(`[${localTrace}] Notify Producer`)
            this.waitingDequeue[this.dequeuePointer - 1]()
            this.dequeuePointer -= 1
        }

        return x
    }
}
Run Code Online (Sandbox Code Playgroud)

更新2:上面的代码中似乎隐藏着一个微妙的错误,在尝试使用AsyncQueue大小为0 时变得明显.语义确实有意义:它是一个没有任何缓冲区的队列,发布者总是等待消费者存在.阻止它工作的线是:

class AsyncSemaphore {
    private promises = Array<() => void>()

    constructor(private permits: number) {}

    signal() {
        this.permits += 1
        if (this.promises.length > 0) this.promises.pop()()
    }

    async wait() {
        if (this.permits == 0 || this.promises.length > 0)
            await new Promise(r => this.promises.unshift(r))
        this.permits -= 1
    }
}

class AsyncQueue<T> {
    private queue = Array<T>()
    private waitingEnqueue: AsyncSemaphore
    private waitingDequeue: AsyncSemaphore

    constructor(readonly maxSize: number) {
        this.waitingEnqueue = new AsyncSemaphore(0)
        this.waitingDequeue = new AsyncSemaphore(maxSize)
    }

    async enqueue(x: T) {
        await this.waitingDequeue.wait()
        this.queue.unshift(x)
        this.waitingEnqueue.signal()
    }

    async dequeue() {
        await this.waitingEnqueue.wait()
        this.waitingDequeue.signal()
        return this.queue.pop()!
    }
}
Run Code Online (Sandbox Code Playgroud)

如果你仔细观察,你会发现它dequeue()并不是完全对称的enqueue().事实上,如果交换这两个指令的顺序:

await this.waitingEnqueue.wait()
this.waitingDequeue.signal()
Run Code Online (Sandbox Code Playgroud)

然后一切再次起作用; 对我来说似乎很直观,我们dequeuing()在实际等待enqueuing发生之前发出了一些感兴趣的东西.

如果没有大量的测试,我仍然不确定这不会重新引入细微的错误.我会把这作为挑战;)