regenerator icon indicating copy to clipboard operation
regenerator copied to clipboard

Should `await null` defer?

Open ghost opened this issue 8 years ago • 8 comments

I've got the following simple code (compiled with Babel+Regenerator) that causes the browser to lock up, but I thought that awaiting inside the while loop would free the CPU for other things in the app to have a chance to execute. The code in the setTimeout never fires, which is unlike what I was expecting to happen, and the app completely locks up:

class TickCounter {
    constructor() {
        this.current = 0
        this.start()
    }

    async start() {
        this.shouldCount = true
        while (this.shouldCount) {
            await null // wait for the next tick.
            this.current += 1
        }
    }

    stop() {
        this.shouldCount = false
    }
}

~async function() {
    let counter = new TickCounter
    console.log('current tick:', counter.current) // is 0
    setTimeout(() => {
        // expecting to see 1, but never fires:
        console.log('current tick:', counter.current)
        counter.stop()
    }, 0)
}()

I am able to achieve what I was intending if I change the self-executing function to

~async function() {
    let counter = new TickCounter
    console.log('current tick:', counter.current) // is 0
    await null
    console.log('current tick:', counter.current) // is 1
    counter.stop()
}()

and in this latter case the while loop is topped and the app doesn't lock up.

Is this an issue with regenerator, or just a way in which async/await in general cannot be used?

ghost avatar Feb 29 '16 02:02 ghost

await null returns an already resolved promise so this is expected behaviour, there's nothing to kick off to the event loop.

sebmck avatar Feb 29 '16 03:02 sebmck

@kittens According to these guys, it should defer. If it's not deferring, then the lockup makes sense.

To defer or not to defer? That is the question. :smile:

ghost avatar Feb 29 '16 03:02 ghost

Here's a cleaner example. I made the TickCounter class in order to try to determine if two pieces of code execute in the same tick or not. Here's a basic example that shows (when compiled with Babel which uses regenerator) that await null or await aResolvedPromise does not defer the code following the await statement:

function sleep(duration) {
    return new Promise(resolve => setTimeout(resolve, duration))
}

class TickCounter {
    constructor() {
        this.current = -1
    }

    async start() {
        this.shouldCount = true
        while (this.shouldCount) {
            this.current += 1
            await sleep(0) // wait for the next tick.
        }
    }

    stop() {
        this.shouldCount = false
    }
}

async function main() {
    let counter = new TickCounter
    counter.start()
    console.log('current tick:', counter.current) // 0

    await null

    console.log('current tick:', counter.current) // 0, still the same tick?
    counter.stop()
}

main()

Now, I'm not entirely sure, but it could be possible that the tick has changed, and that the code after await null just happens to be executing before executions returns to TickCounter's while loop in order to increment the counter.

@erights Says that await statements should always defer code that follows. I myself am not sure whether I would prefer await resolvedPromise to defer following code or not. It seems to me that if a promise is resolved and provides a needed value, then why not give it back to the code right await without deferring to a following tick, and let the user be more explicit in order to defer by using something like await sleep(1). But then again, sleep uses setTimeout, and I'm not sure we can guarantee correct ordering of deferred code if using sleep(1) in one place first and sleep(0) in another place second, all within the same tick. Maybe await statement always deferring following code would be a guarantee that the next (deferred) lines would execute in some defined order. I don't know if I may have overlooked any advantages of always deferring await statements on resolved promises compared to not.

@benjamn Any thoughts?

trusktr avatar Apr 08 '16 15:04 trusktr

My understanding (and preference) is that await x is always identical in behavior to await Promise.resolve(x), so await null should definitely introduce an asynchronous pause. If it continues synchronously in Regenerator, that's a bug.

The always-async approach avoids the dreaded Zalgo problem, in case you haven't been through that thought exercise before. Though await null might seem statically analyzable and harmless enough, imagine await f() where f() sometimes returns null and sometimes returns Promise. Sayonara static analysis!

benjamn avatar Apr 08 '16 15:04 benjamn

@benjamn Thanks for that link!

I just realized that my use of sleep(0) which uses setTimeout(..., 0) doesn't work properly in this test because setTimeout usually fires after 4ms at the earliest (I read that somewhere, can't remember where), and that things like postMessage are much quicker.

How can we make this test work as expected? i.e. Do you know what's the current defacto way to deferr to the soonest possible tick, so I can use that in TickCounter instead?

trusktr avatar Apr 08 '16 15:04 trusktr

@benjamn The following example breaks, which is why I originally opened this post under the possibly wrong assumption that await null isn't deferring, but now I don't think that's the problem as await Promise.resolve(null) also fails. Your Chrome tab will freeze:

class TickCounter {
    constructor() { this.current = -1 }
    async start() {
        this.shouldCount = true
        while (this.shouldCount) { this.current += 1; await null }
    }
    stop() { this.shouldCount = false }
}

const sleep = time => new Promise(r => setTimeout(r, time))

async function main() {
    let counter = new TickCounter
    counter.start()
    console.log('current tick:', counter.current) // is 0

    await sleep(0)

    // these lines never fire.
    console.log('current tick:', counter.current) // was expecting 1
    counter.stop()
}

main()

I found it again: Promise.resolve() is the fastest way to defer. In that crashing example, if we replace await null with await Promise.resolve(null), the freezing still happens.

(That article got me reading about Zalgo, which led me to discover Boxxy. Thanks for the good read! :laughing:)

trusktr avatar Apr 08 '16 16:04 trusktr

Here's the simplest reproduction of the problem I could make (a Meteor 1.3 app): https://github.com/trusktr/site/tree/regenerator-issue-236

To run, just

git clone [email protected]:trusktr/site.git
cd site
git checkout regenerator-issue-236
meteor

You'll notice the behavior is like that of an infinite loop.

trusktr avatar Apr 08 '16 17:04 trusktr

Closing this one, since I deleted the account I used to open this issue, and making a new clean issue: #240.

EDIT: Oh wait, I can't close it.

trusktr avatar Apr 08 '16 19:04 trusktr