为啥流行的 JavaScript 运行时不能处理看起来像同步的异步脚本?

Posted

技术标签:

【中文标题】为啥流行的 JavaScript 运行时不能处理看起来像同步的异步脚本?【英文标题】:Why couldn't popular JavaScript runtimes handle synchronous-looking asynchronous script?为什么流行的 JavaScript 运行时不能处理看起来像同步的异步脚本? 【发布时间】:2014-10-16 06:26:48 【问题描述】:

作为cowboy says down in the comments here,我们都想“以类似这样的风格编写[非阻塞javascript]异步代码:

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

"

所以人们已经想出了解决这个问题的方法,比如

回调库(例如async) promises event patterns streamline domains 和 generators。

但这些都不会导致代码像上面的同步样式代码那样简单易懂。

那么为什么 javascript 编译器/解释器不可能不阻塞我们目前称为“阻塞”的语句? 那么为什么 javascript 编译器/解释器无法处理上面的同步语法,因为我们以异步样式编写了它?”

例如,在处理上面的getSomething() 时,编译器/解释器可以只说“此语句是对 [文件系统/网络资源/...] 的调用,所以我会记下以听取响应从那个电话开始,同时继续我的事件循环中的任何事情”。当调用返回时,可以继续执行到doSomething()

您仍将维护流​​行 JavaScript 运行时环境的所有基本功能

单线程 事件循环 “异步”处理阻塞操作(I/O、网络、等待计时器)

这将只是对语法的调整,这将允许解释器在它检测到异步操作时暂停任何给定代码位的执行,并且不需要回调,代码只是从异步调用之后的行继续当电话返回时。

正如Jeremy 所说

在 JavaScript 运行时中没有任何东西会抢先 暂停给定任务的执行,允许其他代码执行 一会儿,然后恢复原来的任务

为什么不呢? (例如,“为什么没有?”...我对历史课不感兴趣)

为什么开发人员必须关心语句是否阻塞?计算机用于自动化人类不擅长的事情(例如编写非阻塞代码)。

你也许可以用

来实现它 类似 "use noblock"; 的语句(有点像 "use strict";)为一整页代码打开此“模式”。编辑:"use noblock";是一个糟糕的选择,并误导了一些回答者,我试图完全改变常见 JavaScript 运行时的性质。像'use syncsyntax'; 这样的东西可能会更好地描述它。 某种parallel(fn, fn, ...); 语句允许您在"use syncsyntax" 中并行运行事物;模式 - 例如允许一次启动多个异步活动 编辑:一个简单的同步样式语法wait(),将在"use syncsyntax" 中用于代替setTimeout();模式

编辑:

举个例子,而不是写(标准回调版本)

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


fnInsertDB('stuff', fnDeleteDB);

你可以写

'use syncsyntax';

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


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

syncsyntax 版本的处理方式与标准版本完全相同,但更容易理解程序员的意图(只要您了解syncsyntax 会按照所讨论的那样暂停此代码的执行)。

【问题讨论】:

这里有几个答案提到了“竞争条件”。对于这种行为会导致的问题,这实际上并不是正确的术语(因为仍然只有一个线程)。真正的危险是不可预测的reentrancy。从一个老前辈那里得到它:许多 Win32 UI API 是(或曾经是)可重入的,它导致了大量的错误。 @StephenCleary 我阅读了您包含的***链接。这对我来说有点太“comp sci”了,但在我看来,重入本身并不是一件坏事。其定义包括“...并且安全地再次调用”。 Searching for "unpredictable reentrancy" 收效甚微,所以我缺少一些太多的东西,我无法从你的评论中做出一些事情。不过我很想学。如果您有时间,也许您可​​以通过解释如何我的建议会导致不可预测的重入以及为什么这是一件坏事来扩展答案。 Wiki 页面描述了您为可重入而设计的情况。没关系。 “noblock”的问题在于您的代码的所有 必须是安全可重入的(不仅仅是标记为“noblock”的部分)。如果有时间,我会写一个更长的答案。 【参考方案1】:

因为 Javascript 解释器是单线程的、事件驱动的。这就是最初语言的开发方式。

你不能做"use noblock",因为在那个阶段不能进行其他工作。这意味着您的 UI 不会更新。您无法响应用户的鼠标或其他输入事件。您不能重绘屏幕。什么都没有。

所以你想知道为什么?因为 javascript 会导致显示发生变化。如果您能够同时执行这两项操作,您的代码和显示就会遇到所有这些可怕的竞争条件。你可能认为你已经在屏幕上移动了一些东西,但它还没有绘制,或者它绘制了并且你在它绘制之后移动了它,现在它必须再次绘制,等等。这种异步性质允许执行中的任何给定事件堆栈具有已知的良好状态——在执行时不会修改正在使用的数据。

这并不是说你想要的东西不存在,以某种形式。

async library 允许您执行您的parallel 想法(以及其他想法)。

Generators/async/wait 将允许您编写看起来像您想要的代码(尽管它本质上是异步的)。

尽管您在这里提出了错误的主张——人类在编写异步代码方面并不差。

【讨论】:

也许我应该说“为什么 javascript 编译器/解释器无法处理我们知道会创建“块”的同步语法,而是(使用 'use noblock'; 之类的东西)暂停/为我们恢复代码执行,就像我们以异步方式编写它一样?”。即使用同步语法来编写异步代码。当然,“Javascript 解释器是单线程的、事件驱动的”。我完全理解这一点,并不建议应该改变。 “这意味着你的 UI 不会更新。”不。想象一下'use noblock'; var ajaxConfigParams = fnSyncPrepareAjaxCall(); var msg = $.ajax(ajaxConfigParams); fnRenderUI(msg);ajaxConfigParams 将不包含 success: 函数,因为消息 msg(成功时)将从 $.ajax 返回。虽然$.ajax 正在做它的事情,但解释器(因为'use noblock';)知道(a)在事件循环中做任何事情,直到这个异步调用返回(即没有阻塞...... UI 可以继续更新等),并且( b) 不要处理 'fnRenderUI' 直到 '$.ajax' 返回。 这正是 generators/async/wait 试图提供的。你调用异步的东西,你不会打扰回调,但就像它是一个同步块一样继续。 FWIW 这也是 iced coffeescript 和 toffee-script 声称要提供的东西。您以同步方式编写代码,它会为您生成异步 javascript。 还有 @poshest 在你的 ajax 例子中解释器怎么知道你将在那个代码块中做其他事情?它怎么知道在其他一些代码部分中你不会改变 jquery 响应所期望的状态?也许删除更新 UI 事物需要添加它的响应的div【参考方案2】:

那么为什么 javascript 编译器/解释器不可能不阻塞我们目前称为“阻塞”的语句?

因为concurrency control。我们希望阻止它们,以便(在 JavaScript 的单线程性质中)我们可以安全地避免 race conditions 在我们仍在执行它时改变我们函数的状态。我们不能有一个解释器在任意语句/表达式处暂停当前函数的执行,并从程序的某些不同部分继续执行。

例子:

function Bank() 
    this.savings = 0;

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

同步码:

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

异步、任意非阻塞代码:

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

JavaScript 运行时中没有任何东西会抢先暂停给定任务的执行,允许其他代码执行一段时间,然后恢复原始任务

为什么不呢?

为了简单和安全,请参见上文。 (而且,对于历史课:这就是刚刚完成的方式)

然而,这不再是事实。对于 ES6 生成器, 可以让您显式暂停当前 function 生成器的执行:yield 关键字。

随着语言的发展,ES7 还计划使用 asyncawait 关键字。

生成器 [... don't ...] 生成的代码与上面的同步代码一样简单易懂。

但他们有!在那篇文章中甚至是正确的:

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

这里也有一篇关于这个主题的非常好的文章:http://howtonode.org/generators-vs-fibers

【讨论】:

Bergi,对我来说,您的答案取决于“远离竞争条件”的需要。您能否给我一个可能由使用我建议的同步语法引起的竞争条件的实际示例(从根本上做异步的事情)。也许您可以使用我在 tkone 的回答中评论的 $.ajax 示例。 :) 我添加了一个很长的例子。希望对您有所帮助。 感谢您的示例!但正如斯蒂芬上面所说,不会发生竞争条件,因为我们仍然处于单线程环境中(我不是要改变它!)。另外,在我看来,你永远不会使用'use noblock';' just in one function. You'd use it across your entire program, just as with 'use strict';`。 有了精彩的'use noblock';,您的示例变得更具可读性,因此...'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() 将被替换为基本的 wait()(还在我的问题中添加到不断增长的 'use noblock'; 规范中的注释,哈哈) @poshest:是的,控制流(无论以哪种方式表示)都具有相同的竞争条件可能性。您的语法问题是“magically 暗示”。如果你不知道哪些函数是异步的,哪些不是,那么理解代码以及它在哪里暂停将是一件可怕的事情。当您无法控制函数是同步还是异步时(例如 transfer 不知道 howMuch 中的异步),编写正确 & 竞赛将是一件可怕的事情(如果不是完全不可能的话) -无条件代码。这就是生成器强迫我们明确使用yieldfunction* 的原因。【参考方案3】:

其他答案谈到了多线程和并行性引入的问题。但是,我想直接回答您的问题。

为什么不呢? (例如,“为什么没有?”...我对历史课不感兴趣)

绝对没有理由。 ECMAScript - JavaScript 规范没有说明任何关于并发性,它没有指定代码运行的顺序,它没有指定事件循环或事件根本没有,它没有指定任何关于阻塞或不阻塞的内容。

并发在 JavaScript 中的工作方式是由它的宿主环境定义的——例如在浏览器中是 DOM,而 DOM 指定了事件循环的语义。像 setTimeout 这样的“异步”函数只是 DOM 而不是 JavaScript 语言的关注点。

此外,没有什么说 JavaScript 运行时必须运行单线程等等。如果您有顺序代码,则指定执行顺序,但没有什么能阻止任何人将 JavaScript 语言嵌入到多线程环境中。

【讨论】:

好点。在整个过程中将“JavaScript”更改为“流行的 JavaScript 运行时”。【参考方案4】:

为什么不呢?没有理由,只是没有完成。

在 2017 年,它已经在 ES2017 中完成:async functions 可以使用 await 非阻塞地等待承诺的结果。如果getSomething 返回一个promise(注意await)并且如果这是在async 函数中,您可以这样编写代码:

try 

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

    console.error(error);

(我假设您只希望 getSomething 是异步的,但它们都可以。)

实时示例(需要最新的浏览器,例如最近的 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.

您已使用 NodeJS 标记了您的问题。如果您将 Node API 包装在 Promise 中(例如,使用 promisify),您可以编写漂亮的、外观同步的、异步运行的代码。

【讨论】:

你是对的 TJ! async await 正是我想要的!最重要的是我在我的问题中提到的所有解决方案,这最接近于“像上面的同步样式代码一样简单易懂”。去 ES2017!

以上是关于为啥流行的 JavaScript 运行时不能处理看起来像同步的异步脚本?的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript, Ajax - 为啥 JavaScript 代码上的 Ajax 函数不能正常运行?

为啥我的Visual Studio 2010 运行时不能显示汉字?

为啥不能从 Kivy 终止这个 Python 多处理进程?

为啥我们不能在一个批处理文件中执行 BigQuery 的多个语句?

为啥我的 LUA 解释器不能处理字符串键值?

openrefine为啥不能运行