ค้นหาเว็บไซต์

javascript - คิวจำกัดแบบอะซิงโครนัสใน JS/TS โดยใช้ async/await


ฉันกำลังพยายามทำความเข้าใจ 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)
})()

ปัญหาของฉัน แน่นอน อยู่ที่ส่วนของโค้ดที่ "Block until available". ฉันคาดหวังที่จะสามารถ "หยุด" การทำงานจนกว่าจะเกิดบางสิ่งบางอย่าง (เช่น dequeue จะหยุดจนกว่า enqueue จะมีอยู่ และในทางกลับกันถ้ามีพื้นที่ว่าง) ฉันมีความรู้สึกว่าฉันอาจต้องใช้ coroutines สำหรับเรื่องนี้ แต่ฉันอยากมั่นใจว่าฉันไม่ได้พลาดเวทมนตร์ async/await อะไรอยู่

คำตอบ #1

17/04/2019 อัปเดต: พูดให้ง่าย ๆ ก็คือ มีข้อบกพร่องในโค้ด AsyncSemaphore ที่ใช้ด้านล่าง ซึ่งถูกจับได้โดยการทดสอบโดยอิงคุณสมบัติ คุณสามารถอ่านเรื่องราวทั้งหมดเกี่ยวกับ "นิทาน" นี้ได้ที่นี่ นี่คือเวอร์ชันที่แก้ไขแล้ว:

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))
    }
}

สุดท้าย หลังจากความพยายามอย่างมาก และได้รับแรงบันดาลใจจากคำตอบของ @Titian ฉันคิดว่าฉันแก้ปัญหานี้ได้แล้ว โค้ดนี้เต็มไปด้วยข้อความดีบัก แต่ก็อาจมีประโยชน์ในเชิง การสอน เกี่ยวกับลำดับการควบคุม:

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
    }
}

อัปเดต: นี่คือเวอร์ชันที่สะอาดโดยใช้ AsyncSemaphore ซึ่งแสดงถึงวิธีที่สิ่งต่าง ๆ มักทำโดยใช้ primitives ของการแข่งขัน แต่ปรับให้เข้ากับสไตล์ asynchronous-CPS-single-threaded-event-loop™ ของ JavaScript ด้วย async/await คุณจะเห็นว่า logic ของ AsyncQueue กลายเป็นเรื่องที่เข้าใจได้มากขึ้น และการซิงโครไนซ์ซ้ำสองผ่าน Promises จะถูกมอบหมายให้กับสอง semaphore:

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()!
    }
}

อัปเดต 2: ดูเหมือนว่าจะมีข้อบกพร่องเล็ก ๆ ซ่อนอยู่ในโค้ดข้างต้น ซึ่งปรากฏชัดเมื่อพยายามใช้ AsyncQueue ขนาด 0 ความหมายของมันก็สมเหตุสมผล: มันคือคิวที่ไม่มีบัฟเฟอร์ โดยที่ผู้เผยแพร่จะต้องรอให้ผู้บริโภคมีอยู่เสมอ บรรทัดที่ทำให้มันไม่ทำงานคือ:

await this.waitingEnqueue.wait()
this.waitingDequeue.signal()

ถ้าคุณสังเกตอย่างใกล้ชิด คุณจะเห็นว่า dequeue() ไม่สมมาตรอย่างสมบูรณ์กับ enqueue() แท้จริงแล้ว หากมีการสลับลำดับของคำสั่งทั้งสองนี้:

this.waitingDequeue.signal()
await this.waitingEnqueue.wait()

จากนั้นทุกอย่างก็ทำงานได้อีกครั้ง; มันดูเป็นเรื่องสัญชาตญาณสำหรับฉันที่เราส่งสัญญาณว่า มีบางสิ่งสนใจใน dequeuing() ก่อนที่จะรอให้เกิด enqueuing จริง ๆ

ฉันยังไม่แน่ใจว่านี่จะไม่กลับไปสร้างบั๊กเล็ก ๆ โดยไม่ได้ทดสอบอย่างละเอียด ฉันจะปล่อยให้เป็นความท้าทาย ;)

บทความที่เกี่ยวข้อง