6 ms·
I tried this exact thing last year, and it does not work for a particular reason: the JavaScript event loop guarantees that a function will run to the end befor
by ermir 5y ago
I tried this exact thing last year, and it does not work for a particular reason: the JavaScript event loop guarantees that a function will run to the end before any callbacks will run, but an await is an implicit callback (a Promise under the hood). This means that you break the guarantee that variables or other memory locations will have the same value between statements.
This is exactly the same problem that languages such as C++ have when dealing with multithreading, and why constructs such as semaphores, mutexes, etc. exist. JS does not have these, and using the technique in the article will completely break the language.
Compare these two fiddles I prepared and see for yourself:
This one uses await to block for a second, which in the meantime changes the value of the variable between statements: https://jsfiddle.net/gq1btLdu/ https://jsfiddle.net/gq1btLdu/
This one uses a for loop to do synchronous work for about a second, then prints the initial value twice. You will notice that the setInterval never had the chance to run, since callbacks are scheduled to run only once the current function ends: https://jsfiddle.net/gq1btLdu/1/ https://jsfiddle.net/gq1btLdu/1/
- protoduction 5y agoYou are right that it's a terrible idea (I hope it was clear from the article that it's an experiment and should stay that way), but could you help me understand why this is fundamentally broken? I would say that by using setInterval you are buying into this kind of behavior and would have to create your own synchronization. If you are saying that this transpilation step changes the semantics of the code, then I completely agree!
- ermir 5y agoIf you have code like: let x = 0; doSomethingAsync(); // does not touch x console.log(x); There's no guarantee that x will have the value 0 by the time you get to console.log(x). The value of x might have been changed by an outside function that just happened to run between your statements. In synchronous JS this would never happen as callbacks are scheduled to run only at the end of a function, and if they change x the change will only be applied after x is printed. The change in behavior will break a lot of existing code, since the synchronous behavior of a function is a core feature and assumption of the language.
- tines 5y agoIsn't that only really a problem with global variables? Local variables would be safe from this kind of behavior I'd think.
- protoduction 5y agoThank you for taking the time to explain :) I completely see that this will break existing code, but I don't see how this is different from the behavior of Go (where the setTimeout code would be something in a different goroutine) or Python?
- thysultan 5y agoThat is false except for the case that x is a global variable, no outside function can change the value of x, only in-scope procedures can do that.
- ermir 5y agoI guess it is true in this particular example, since 0 is a primitive and assigned by value. But what if you got it from an outside function, such as init(), and it was an object instead? Then you would have to guarantee that the return value of init() would not change between statements. In the end this shows that JS is unequipped to handle such behavior without significant changes to the core language.
- brundolf 5y ago> But what if you got it from an outside function, such as init(), and it was an object instead? Then you would have to guarantee that the return value of init() would not change between statements. Then even in a synchronous context you'd have no guarantee that it wouldn't be modified: function soSomethingSync() { window.someObj.someProp = 12; } let x = window.someObj; soSomethingSync(); console.log(x) This has nothing whatsoever to do with sync vs async. It is one of the main reasons people like immutability/functional programming. But that's its own topic.
- 5y ago
- wolfgang42 5y agoFor a practical example, take this code I wrote recently:[1] const backends = {} async function setupBackend(host) { /* boot server */ } function getBackend(host) { if (!backends[host]) { backends[host] = setupBackend(host) } return backends[host] } That if block is effectively a critical section: it relies on explicitly not awaiting the result of setupBackend(), so that the promise will be stored into backends[host] (to be reused by subsequent calls to getBackend()) before anything else can happen. Injecting `async` will break this behavior, causing the backend to be booted multiple times. [1] https://github.com/wolfgang42/webd/blob/8cf28447468dd4745262800ec0fc5a6e9da2aa09/webd.js#L127-L132 https://github.com/wolfgang42/webd/blob/8cf28447468dd4745262...
- pkage 5y agoWhy not simply set some intermediate value to indicate that the backend is booting? Such as: const backends = {} async function setupBackend(host) { /* boot server */ } function getBackend(host) { if (!backends[host]) { backends[host] = 'FLAG' // or something setupBackend(host) .then(backend => backends[host] = backend) } else if (backends[host] === 'FLAG') { // no-op } else { return backends[host] } }
- wolfgang42 5y agoSubsequent callers of getBackend also need to be able to await on the backend boot before they can do anything useful. If the backend is currently booting, your function will return undefined in the “no-op” case, giving the caller no way to tell when it’s finished (other than retrying, I suppose). I tried writing some code for this comment that used an explicitly constructed Promise as the intermediate value with some logic to resolve it once setup was complete, but then I realized that it had exactly the same problem with that being implicitly waited on. Maybe there’s some clever way to work around this but it’s going to be a lot more complicated. Of course, if you do insist on a JS dialect with implicit await, the easy fix for this problem (since you’re transpiling anyway) is to just introduce a `noawait` keyword that turns the await insertion off for a block, to explicitly mark it as atomic. [ETA: also, in the general case there’s a race condition if someone calls the function again while it’s awaiting the initial flag value. That doesn’t happen with your code because the OP library happens to not await literals, but that’s kind of fragile: I can easily see a situation where someone tries to introduce e.g. a counter into the flag and causes a non-obvious race.]
- brundolf 5y agoSorry, but you've got some fundamental misconceptions here. First off - and this is going to sound pedantic, but it's important to get our terminology straight - a "callback" is any function that gets passed to another function so that it can be "called back to" later on. It is entirely possible for this to happen synchronously. Example: function doACallback(theCallback) { theCallback() } doACallback(() => console.log('hello')) console.log('world') This code runs synchronously; the event loop is not involved at all. It will print "hello" and then "world". Now, the most common usage of callbacks is for various asynchronous things that happen on the event loop. Input events, setTimeout/setInterval, and yes, Promises. However, async/await only concerns itself with Promises. Not with setTimeout or setInterval or events. So if you want to use it to take something asynchronous and make it appear/behave as if it were synchronous, that thing needs to be in the form of a Promise. You demonstrate this in your fiddle by wrapping setTimeout in a Promise inside the sleep() function. But, you then go and use a setInterval, which is totally outside the domain of Promises, and so async/await has nothing to say about it. Your example only demonstrates that by "doing something async" you're putting the ordering of certain things at the mercy of the event loop. In practice, the answer to this "problem" is that if the ordering of asynchronous things matters to your logic, then that order needs to be enforced via .then() chaining or async/await or otherwise. Never just rely on the ordering of the event loop itself. If you do, then you already have a race condition, whether you've realized it or not. Both of the code samples would be problematic if you ever did this in production. Further: this is most definitely not the same problem that C++ solves with semaphores, mutexes, etc. Both languages can have race conditions, but JavaScript cannot have data races, which is what those constructs exist to deal with. That is why JavaScript doesn't have them. To get specific: in JavaScript, only one thread can actually be running (with the memory space these variables exist in) at a time. The async stuff may make this less than obvious, but it's a firm truth. JavaScript code can't be interrupted arbitrarily, it can only yield control of the thread at a given await. You might argue that making this invisible might make it easier for people to write those kinds of bugs without noticing, but it certainly wouldn't "completely break the language".
- brundolf 5y agoEdit: I should clarify that technically making everything async/await like this would change the behavior of existing code in a subtle way. It's just that realistically, the behavior that's changing is not behavior you should ever rely on in the first place. It technically isn't undefined behavior, but good code would treat it as if it were.