两个或多个“进程”同时执行就出现了并发,不管组成它们的单个运算是否并行执行(在独立的处理器或处理器核心上同时运行)。可以把并发看作“进程”级(或者任务级)的并行,与运算级的并行(不同处理器上的线程)相对。
单线程事件循环是并发的一种形式。
非交互
两个或多个“进程”在同一个程序内并发地交替运行它们的步骤 / 事件时,如果这些任务彼此不相关,就不一定需要交互。如果进程间没有相互影响的话,不确定性是完全可以接受的。
var res = {};function foo(results) {res.foo = results;}function bar(results) {res.bar = results;}// ajax(..)是某个库提供的某个Ajax函数ajax( "http://some.url.1", foo );ajax( "http://some.url.2", bar );
foo() 和 bar() 是两个并发执行的“进程”,按照什么顺序执行是不确定的。但是,我们构建程序的方式使得无论按哪种顺序执行都无所谓,因为它们是独立运行的,不会相互影响。这并不是竞态条件 bug,因为不管顺序如何,代码总会正常工作。
交互
更常见的情况是,并发的“进程”需要相互交流,通过作用域或 DOM 间接交互。
下面是一个简单的例子,两个并发的“进程”通过隐含的顺序相互影响,这个顺序有时会被破坏:
var res = [];function response(data) {res.push( data );}// ajax(..)是某个库中提供的某个Ajax函数ajax( "http://some.url.1", response );ajax( "http://some.url.2", response );
这里的并发“进程”是这两个用来处理 Ajax 响应的 response() 调用。它们可能以任意顺序运行。
我们假定期望的行为是 res[0] 中放调用 "http://some.url.1" 的结果,res[1] 中放调用 "http://some.url.2" 的结果。有时候可能是这样,但有时候却恰好相反,这要视哪个调用先完成而定。这种不确定性很有可能就是一个竞态条件 bug。
可以协调交互顺序来处理这样的竞态条件:
var res = [];function response(data) {if (data.url == "http://some.url.1") {res[0] = data;}else if (data.url == "http://some.url.2") {res[1] = data;}}// ajax(..)是某个库中提供的某个Ajax函数ajax( "http://some.url.1", response );ajax( "http://some.url.2", response );
通过简单的协调,就避免了竞态条件引起的不确定性。
有些并发场景如果不做协调,就总是(并非偶尔)会出错:
var a, b;function foo(x) {a = x * 2;baz(); }function bar(y) {b = y * 2;baz(); }function baz() {console.log(a + b);}// ajax(..)是某个库中的某个Ajax函数ajax( "http://some.url.1", foo );ajax( "http://some.url.2", bar );
无论 foo() 和 bar() 哪一个先被触发,总会使 baz() 过早运行(a 或者 b 仍处于未定义状态);但对 baz() 的第二次调用就没有问题,因为这时候 a 和 b 都已经可用了。
一种简单方法:
var a, b;function foo(x) {a = x * 2;if (a && b) {baz();}}function bar(y) {b = y * 2;if (a && b) {baz();}}function baz() {console.log( a + b );}// ajax(..)是某个库中的某个Ajax函数ajax( "http://some.url.1", foo );ajax( "http://some.url.2", bar );
包裹baz()调用的条件判断if (a && b)传统上称为门(gate),虽然不能确定a和b 到达的顺序,但是会等到它们两个都准备好再进一步打开门(调用 baz())。
另一种可能遇到的并发交互条件有时称为竞态(race),但是更精确的叫法是门闩(latch)。 它的特性可以描述为“只有第一名取胜”。在这里,不确定性是可以接受的,因为它明确指出了这一点是可以接受的:需要“竞争”到终点,且只有唯一的胜利者。
var a;function foo(x) {a = x * 2;baz(); }function bar(x) {a = x / 2;baz(); }function baz() {console.log( a );}// ajax(..)是某个库中的某个Ajax函数ajax( "http://some.url.1", foo );ajax( "http://some.url.2", bar );
不管哪一个(foo() 或 bar())后被触发,都不仅会覆盖另外一个给 a 赋的值,也会重复调用 baz()(很可能并不是想要的结果)。
可以通过一个简单的门闩协调这个交互过程,只让第一个通过:
var a;function foo(x) {if (!a) {a = x * 2;baz(); }}function bar(x) {if (!a) {a = x / 2;baz(); }}function baz() {console.log( a );}// ajax(..)是某个库中的某个Ajax函数ajax( "http://some.url.1", foo );ajax( "http://some.url.2", bar );
条件判断if (!a)使得只有foo()和bar()中的第一个可以通过,第二个(实际上是任何后续的)调用会被忽略。也就是说,第二名没有任何意义!
协作
还有一种并发合作方式,称为并发协作(cooperative concurrency)。这里的重点不再是通过 共享作用域中的值进行交互(尽管显然这也是允许的!)。这里的目标是取到一个长期运行的“进程”,并将其分割成多个步骤或多批任务,使得其他并发“进程”有机会将自己的运算插入到事件循环队列中交替运行。
举例:考虑一个需要遍历很长的结果列表进行值转换的 Ajax 响应处理函数。我们会使用 Array.prototype.map(..) 让代码更简洁:
var res = [];// response(..)从Ajax调用中取得结果数组function response(data) {// 添加到已有的res数组res = res.concat(// 创建一个新的变换数组把所有data值加倍data.map( function(val){return val * 2;}));}// ajax(..)是某个库中提供的某个Ajax函数ajax( "http://some.url.1", response );ajax( "http://some.url.2", response );
如果 "http://some.url.1" 首先取得结果,那么整个列表会立刻映射到 res 中。如果记录有几千条或更少,这不算什么。但是如果有像 1000 万条记录的话,就可能需要运行相当一段时间了。这样的“进程”运行时,页面上的其他代码都不能运行,包括不能有其他的 response(..) 调用或 UI 刷新,甚至是像滚动、输入、按钮点击这样的用户事件。所以,要创建一个协作性更强更友好且不会霸占事件循环队列的并发系统,可以异步地批处理这些结果。每次处理之后返回事件循环,让其他等待事件有机会运行。
var res = [];// response(..)从Ajax调用中取得结果数组function response(data) {// 一次处理1000个var chunk = data.splice( 0, 1000 );// 添加到已有的res组res = res.concat(// 创建一个新的数组把chunk中所有值加倍chunk.map( function(val){return val * 2;}));// 还有剩下的需要处理吗?if (data.length > 0) {// 异步调度下一次批处理setTimeout( function(){response( data );}, 0 );}}// ajax(..)是某个库中提供的某个Ajax函数ajax( "http://some.url.1", response );ajax( "http://some.url.2", response );
我们把数据集合放在最多包含 1000 条项目的块中。这样,就确保了“进程”运行时间会很短,即使这意味着需要更多的后续“进程”,因为事件循环队列的交替运行会提高站点 /App 的响应(性能)。
这里使用 setTimeout(..0)(hack)进行异步调度,基本上它的意思就是“把这个函数插入到当前事件循环队列的结尾处”。
