regenerator
regenerator copied to clipboard
Should `await null` defer?
I've got the following simple code (compiled with Babel+Regenerator) that causes the browser to lock up, but I thought that await
ing 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?
await null
returns an already resolved promise so this is expected behaviour, there's nothing to kick off to the event loop.
@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:
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?
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 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?
@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:)
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.
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.