两个或多个“进程”同时执行就出现了并发,不管组成它们的单个运算是否并行执行(在独立的处理器或处理器核心上同时运行)。可以把并发看作“进程”级(或者任务级)的并行,与运算级的并行(不同处理器上的线程)相对。
单线程事件循环是并发的一种形式。
非交互
两个或多个“进程”在同一个程序内并发地交替运行它们的步骤 / 事件时,如果这些任务彼此不相关,就不一定需要交互。如果进程间没有相互影响的话,不确定性是完全可以接受的。
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)进行异步调度,基本上它的意思就是“把这个函数插入到当前事件循环队列的结尾处”。