Why couldn't popular JavaScript runtimes handle synchronous-looking asynchronous script?
Asked Answered
F

4

8

As cowboy says down in the comments here, we all want to "write [non-blocking JavaScript] asynchronous code in a style similar to this:

 try 
 {
    var foo = getSomething();   // async call that would normally block
    var bar = doSomething(foo);  
    console.log(bar); 
 } 
 catch (error) 
 {
    console.error(error);
 }

"

So people have come up solutions to this problem like

But none of these lead to code as simple and easy to understand as the sync-style code above.

So why isn't possible for javascript compilers/interpreters to just NOT block on the statements we currently know as "blocking"? So why isn't possible for javascript compilers/interpreters to handle the sync syntax above AS IF we'd written it in an async style?"

For example, upon processing getSomething() above, the compiler/interpreter could just say "this statement is a call to [file system/network resource/...], so I'll make a note to listen to responses from that call and in the meantime get on with whatever's in my event loop". When the call returns, execution can proceed to doSomething().

You would still maintain all of the basic features of popular JavaScript runtime environments

  • single threaded
  • event loop
  • blocking operations (I/O, network, wait timers) handled "asynchronously"

This would be simply a tweak to the syntax, that would allow the interpreter to pause execution on any given bit of code whenever IT DETECTS an async operation, and instead of needing callbacks, code just continues from the line after the async call when the call returns.

As Jeremy says

there is nothing in the JavaScript runtime that will preemptively pause the execution of a given task, permit some other code to execute for a while, and then resume the original task

Why not? (As in, "why couldn't there be?"... I'm not interested in a history lesson)

Why does a developer have to care about whether a statement is blocking or not? Computers are for automating stuff that humans are bad at (eg writing non-blocking code).

You could perhaps implement it with

  • a statement like "use noblock"; (a bit like "use strict";) to turn this "mode" on for a whole page of code. EDIT: "use noblock"; was a bad choice, and misled some answerers that I was trying to change the nature of common JavaScript runtimes altogether. Something like 'use syncsyntax'; might better describe it.
  • some kind of parallel(fn, fn, ...); statement allowing you to run things in parallel while in "use syncsyntax"; mode - eg to allow multiple async activities to be kicked off at once
  • EDIT: a simple sync-style syntax wait(), which would be used instead of setTimeout() in "use syncsyntax"; mode

EDIT:

As an example, instead of writing (standard callback version)

function fnInsertDB(myString, fnNextTask) {
  fnDAL('insert into tbl (field) values (' + myString + ');', function(recordID) {
    fnNextTask(recordID);
  });
}

fnInsertDB('stuff', fnDeleteDB);

You could write

'use syncsyntax';

function fnInsertDB(myString) {
  return fnDAL('insert into tbl (field) values (' + myString ');');  // returns recordID
}

var recordID = fnInsertDB('stuff'); 
fnDeleteDB(recordID);

The syncsyntax version would process exactly the same way as the standard version, but it's much easier to understand what the programmer intended (as long as you understand that syncsyntax pauses execution on this code as discussed).

Fennec answered 22/8, 2014 at 11:51 Comment(3)
A couple of answers here mention "race conditions". That's not actually the correct term for the problem that this kind of behavior would cause (since there's still only one thread). The real danger is unpredictable reentrancy. Take it from an old-timer: much of the Win32 UI APIs are (or used to be) reentrant, and it caused tons and tons and tons of bugs.Monitorial
@StephenCleary I read the Wikipedia link you included. It's a little too "comp sci" for me, but it seems to me that reentrancy isn't a bad thing per se. The very definition includes "... and safely called again". Searching for "unpredictable reentrancy" doesn't yield much, so there's a few too many pieces missing for me to make something out of your comment. I'm keen to learn though. If you find a moment, perhaps you could expand in an answer by explaining how my suggestion would cause unpredictable reentrancy and why it is a bad thing.Fennec
The Wiki page is describing situations where you design for reentrancy. That's fine. The problem with "noblock" is that all of your code must be safely reentrant (not just the part marked "noblock"). I'll write up a longer answer later if I have time.Monitorial
S
1

Why not? No reason, it just hadn't been done.

And here in 2017, it has been done in ES2017: async functions can use await to wait, non-blocking, for the result of a promise. You can write your code like this if getSomething returns a promise (note the await) and if this is inside an async function:

try 
{
    var foo = await getSomething();
    var bar = doSomething(foo);  
    console.log(bar); 
} 
catch (error) 
{
    console.error(error);
}

(I've assumed there that you only intended getSomething to be asynchronous, but they both could be.)

Live Example (requires up-to-date browser like recent Chrome):

function getSomething() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (Math.random() < 0.5) {
                reject(new Error("failed"));
            } else {
                resolve(Math.floor(Math.random() * 100));
            }
        }, 200);
    });
}
function doSomething(x) {
    return x * 2;
}
(async () => {
    try 
    {
        var foo = await getSomething();
        console.log("foo:", foo);
        var bar = doSomething(foo);  
        console.log("bar:", bar); 
    } 
    catch (error) 
    {
        console.error(error);
    }
})();
The first promise fails half the time, so click Run repeatedly to see both failure and success.

You've tagged your question with NodeJS. If you wrap the Node API in promises (for instance, with promisify), you can write nice straight-forward synchronous-looking code that runs asynchronously.

Selfabasement answered 7/9, 2017 at 6:8 Comment(1)
You're right TJ! async await is exactly what I wanted! Above all the solutions I mentioned in my question, this comes closest to being as "simple and easy to understand as the sync-style code above". Go ES2017!Fennec
M
13

So why isn't possible for javascript compilers/interpreters to just NOT block on the statements we currently know as "blocking"?

Because of concurrency control. We want them to block, so that (in JavaScript's single-threaded nature) we are safe from race conditions that alter the state of our function while we still are executing it. We must not have an interpreter that suspends the execution of the current function at any arbitrary statement/expression and resumes with some different part of the program.

Example:

function Bank() {
    this.savings = 0;
}
Bank.prototype.transfer = function(howMuch) {
    var savings = this.savings;
    this.savings = savings + +howMuch(); // we expect `howMuch()` to be blocking
}

Synchronous code:

var bank = new Bank();
setTimeout(function() {
    bank.transfer(prompt); // Enter 5
    alert(bank.savings);   // 5
}, 0);
setTimeout(function() {
    bank.transfer(prompt); // Enter 3
    alert(bank.savings);   // 8
}, 100);

Asynchronous, arbitrarily non-blocking code:

function guiPrompt() {
    "use noblock";
    // open form
    // wait for user input
    // close form
    return input;
}
var bank = new Bank(); 
setTimeout(function() {
    bank.transfer(guiPrompt); // Enter 5
    alert(bank.savings);      // 5
}, 0);
setTimeout(function() {
    bank.transfer(guiPrompt); // Enter 3
    alert(bank.savings);      // 3 // WTF?!
}, 100);

See https://glyph.twistedmatrix.com/2014/02/unyielding.html for a longer (and language-agnostic) explanation.

there is nothing in the JavaScript runtime that will preemptively pause the execution of a given task, permit some other code to execute for a while, and then resume the original task

Why not?

For simplicity and security, see above. (And, for the history lesson: That's how it just was done)

However, this is no longer true. With ES6 generators, there is something that lets you explicitly pause execution of the current function generator: the yield keyword.

As the language evolves, there are also async and await keywords planned for ES7.

generators [… don't …] lead to code as simple and easy to understand as the sync code above.

But they do! It's even right in that article:

suspend(function* () {
//              ^ "use noblock" - this "function" doesn't run continuously
    try {
        var foo = yield getSomething();
//                ^^^^^ async call that does not block the thread
        var bar = doSomething(foo);  
        console.log(bar); 
    } catch (error) {
        console.error(error);
    }
})

There is also a very good article on this subject here: http://howtonode.org/generators-vs-fibers

Marmoreal answered 22/8, 2014 at 12:40 Comment(14)
Bergi, for me your answer hangs on the need to be "safe from race conditions". Can you please give me a practical example of a race condition that might be caused by using the sync syntax I'm suggesting (to do fundamentally async stuff). Perhaps you could use the $.ajax example I commented in tkone's answer. :)Fennec
I've added a lengthy example. Hope it helps.Marmoreal
Thanks for your example! But as Stephen says above, a race condition wouldn't occur because we're still in a single threaded environment (I'm not asking to change that!). Also, in my mind, you wouldn't ever use 'use noblock';' just in one function. You'd use it across your entire program, just as with 'use strict';`.Fennec
With the wonderful 'use noblock';, your example becomes much more readable thus... 'use noblock'; var bank = new Bank(); function fnXferAndAlert (prompt_wait) {wait(prompt_wait); bank.transfer(guiPrompt); alert(bank.savings);}; parallel(fnXferAndAlert(0), fnXferAndAlert(100));. setTimeout() would be replaced with a basic wait() (also added that note to the growing 'use noblock'; specification in my question, lol)Fennec
A race condition does not require multithreading to occur, it just requires concurrency. The point is that transfer does expect howMuch to be blocking, and if suddenly it is not (but the execution is interleaved with something else, occuring asynchrously) then transfer fails. The setTimeout was only used for some event-triggered processing, it could be a click handler as well.Marmoreal
sure you can have race conditions within your entire system (in this question, I refer to Guilro's answer), but your variable "savings" is being accessed by the same single javascript thread, so no race condition according to his logic. On the other hand the example Guilro gives about an insert and delete being fired to a DB would be BETTER handled with my syntax, because even a novice programmer could write var recordID = fnInsertDB("stuff"); fnDeleteDB(recordID); and get exactly what they expected - no race condition.Fennec
"your variable "savings" is being accessed by the same single javascript thread, so no race condition according to his logic". Yes, according to his logic and the standard javascript behaviour, and it is what everyone would expect. But it's exactly that logic that is invalidated by your proposed "use noblock" feature!Marmoreal
The problem with your syntax in the DB example that one would expect that the record is inserted and immediately deleted, so it might not be visible to other code that accesses the DB at all. But that's not true! There is a chance that fnQueryDB("stuff") running in parallel will see it - or not: that's the race condition.Marmoreal
Nice answer, enjoy your much deserved 100K :)Thumbnail
I added an example to my question. Don't both methods introduce the same chance of race conditions? Both code snippets should execute in the same way (by definition). They are just different ways of instructing the single threaded event looped interpreter to do the same thing. So in either case, if the programmer understands that the database update takes time, and that in between, other things can happen, she'll understand the race condition problem and code accordingly.Fennec
Another way of putting it is, take the generators example you made, and imagine that 'use syncsyntax'; (renamed from noblock) magically implies suspend(function* () {}) around your entire code, and implies yield in front of all async function calls. Same result. No boilerplate.Fennec
@poshest: Yes, that control flow - expressed either way - would have the same race condition possibilities. The problem with your syntax is "magically implies". When you don't know which functions are async and which are not, it will be a horror to understand code and where it pauses. When you don't have control over whether a function is sync or async (such as transfer doesn't know about the asynchrony in howMuch), it will be a horror (if not right impossible) to write correct & race-condition-free code. That's why generators force us to be explicit with yield and function*.Marmoreal
Also, "around your entire code" doesn't work, as for writing correct code we would need the option to make sure that statements are run without pauses - not only to implement semaphores. That's why we have a synchronized keyword in Java for example.Marmoreal
@Marmoreal +1 re "magically implies". That, for mine, is definitely a potential problem, but only for programmers unfamiliar with 'use syncsyntax';. My sense is that practically, the benefits of writing in this style far outweigh the odd race condition that might occur. Most real-world code either doesn't cause race conditions OR if they do, they don't cause problems, OR they're proactively solved with DB transactions or transaction log patterns, etc. People with race condition-prone use cases can always just NOT use 'use syncsyntax';.Fennec
C
1

Because Javascript interpreters are single-threaded, event driven. This is how the initial language was developed.

You can't do "use noblock" because no other work can occur during that phase. This means your UI will not update. You cannot respond to mouse or other input event from the user. You cannot redraw the screen. Nothing.

So you want to know why? Because javascript can cause the display to change. If you were able to do both simultaneously you'd have all these horrible race conditions with your code and the display. You might think you've moved something on the screen, but it hasn't drawn, or it drew and you moved it after it drew and now it's gotta draw again, etc. This asynchronous nature allows, for any given event in the execution stack to have a known good state -- nothing is going to modify the data that is being used while this is being executed.

That is not to say what you want doesn't exist, in some form.

The async library allows you to do things like your parallel idea (amongst others).

Generators/async/wait will allow you to write code that LOOKS like what you want (although it'll be asynchronous by nature).

Although you are making a false claim here -- humans are NOT bad at writing asynchronous code.

Cremator answered 22/8, 2014 at 12:11 Comment(7)
Maybe I should have said "why isn't possible for javascript compilers/interpreters to handle the sync syntax that we know creates "blocks", and instead (with something like 'use noblock'; on) pause/resume code execution for us AS IF we'd written it in an async style?". Ie use the sync SYNTAX to write async code. Of course "Javascript interpreters are single-threaded, event driven". I understand that perfectly and am not suggesting that should be changed.Fennec
"This means your UI will not update." No. Imagine 'use noblock'; var ajaxConfigParams = fnSyncPrepareAjaxCall(); var msg = $.ajax(ajaxConfigParams); fnRenderUI(msg);. ajaxConfigParams would contain no success: function, because the message msg (upon success) would be returned from $.ajax. While $.ajax is off doing its thing, interpreter (because of 'use noblock';) knows (a) do whatever in the event loop until this async call returns (ie NO BLOCKING... UI can continue to update etc), and (b) don't process the 'fnRenderUI' UNTIL '$.ajax' has returned.Fennec
This is exactly what generators/async/wait will attempt to provide. You call something that is async and you don't bother will callbacks, but just continue as if it were a synchronous block.Cremator
FWIW this is also something things like iced coffeescript and toffee-script purport to provide. You write you code in a synchronous style and it generates async javascript for you.Cremator
Also @Fennec in your ajax example how does the interpreter know you're going to do something else in that block of code? How does it know that in some other section of code you're not going to mutate the state that the jquery response is expecting? maybe delete a div that the update UI thing needs to add it's response?Cremator
Re "This is exactly what generators...", yes, Bergi says the same. But I've yet to find a simple explanation of how these work. I think my suggested syntax is far simpler, and you don't have to understand all this "a generator is a special type of function that works as a factory for iterators..." (what the!?)Fennec
Re "...how does the interpreter know...?". As I said "interpreters just say 'this statement is a call to [file system/network resource/...], so I'll make a note to listen to responses from that call and in the meantime get on with whatever's in my event loop'. When the call returns, execution can proceed [to process fnRenderUI(msg)]." "maybe delete a div..." the same thing that would happen if you wrote a regular async callback and something else deleted the div before the callback completed. Handle it with if ($('#myDiv').length > 0)... within fnRenderUI() or whatever.Fennec
T
1

The other answers talked about the problems multi-threading and parallelism introduce. However, I want to address your answer directly.

Why not? (As in, "why couldn't there be?"... I'm not interested in a history lesson)

Absolutely no reason. ECMAScript - the JavaScript specification says nothing about concurrency, it does not specify the order code runs in, it does not specify an event loop or events at all and it does not specify anything about blocking or not blocking.

The way concurrency works in JavaScript is defined by its host environment - in the browser for example that's the DOM and the DOM specifies the semantics of the event loop. "async" functions like setTimeout are only the concern of the DOM and not the JavaScript language.

Moreover there is nothing that says JavaScript runtimes have to run single threaded and so on. If you have sequential code the order of execution is specified, but there is nothing stopping anyone from embedding the JavaScript language in a multi threaded environment.

Thumbnail answered 24/8, 2014 at 8:56 Comment(1)
Good point. Changed "JavaScript" to "popular JavaScript runtimes" throughout.Fennec
S
1

Why not? No reason, it just hadn't been done.

And here in 2017, it has been done in ES2017: async functions can use await to wait, non-blocking, for the result of a promise. You can write your code like this if getSomething returns a promise (note the await) and if this is inside an async function:

try 
{
    var foo = await getSomething();
    var bar = doSomething(foo);  
    console.log(bar); 
} 
catch (error) 
{
    console.error(error);
}

(I've assumed there that you only intended getSomething to be asynchronous, but they both could be.)

Live Example (requires up-to-date browser like recent Chrome):

function getSomething() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (Math.random() < 0.5) {
                reject(new Error("failed"));
            } else {
                resolve(Math.floor(Math.random() * 100));
            }
        }, 200);
    });
}
function doSomething(x) {
    return x * 2;
}
(async () => {
    try 
    {
        var foo = await getSomething();
        console.log("foo:", foo);
        var bar = doSomething(foo);  
        console.log("bar:", bar); 
    } 
    catch (error) 
    {
        console.error(error);
    }
})();
The first promise fails half the time, so click Run repeatedly to see both failure and success.

You've tagged your question with NodeJS. If you wrap the Node API in promises (for instance, with promisify), you can write nice straight-forward synchronous-looking code that runs asynchronously.

Selfabasement answered 7/9, 2017 at 6:8 Comment(1)
You're right TJ! async await is exactly what I wanted! Above all the solutions I mentioned in my question, this comes closest to being as "simple and easy to understand as the sync-style code above". Go ES2017!Fennec

© 2022 - 2024 — McMap. All rights reserved.