两个或多个“进程”同时执行就出现了并发,不管组成它们的单个运算是否并行执行(在独立的处理器或处理器核心上同时运行)。可以把并发看作“进程”级(或者任务级)的并行,与运算级的并行(不同处理器上的线程)相对。
单线程事件循环是并发的一种形式。

非交互

两个或多个“进程”在同一个程序内并发地交替运行它们的步骤 / 事件时,如果这些任务彼此不相关,就不一定需要交互。如果进程间没有相互影响的话,不确定性是完全可以接受的。

  1. var res = {};
  2. function foo(results) {
  3. res.foo = results;
  4. }
  5. function bar(results) {
  6. res.bar = results;
  7. }
  8. // ajax(..)是某个库提供的某个Ajax函数
  9. ajax( "http://some.url.1", foo );
  10. ajax( "http://some.url.2", bar );

foo()bar() 是两个并发执行的“进程”,按照什么顺序执行是不确定的。但是,我们构建程序的方式使得无论按哪种顺序执行都无所谓,因为它们是独立运行的,不会相互影响。这并不是竞态条件 bug,因为不管顺序如何,代码总会正常工作。

交互

更常见的情况是,并发的“进程”需要相互交流,通过作用域或 DOM 间接交互。
下面是一个简单的例子,两个并发的“进程”通过隐含的顺序相互影响,这个顺序有时会被破坏:

  1. var res = [];
  2. function response(data) {
  3. res.push( data );
  4. }
  5. // ajax(..)是某个库中提供的某个Ajax函数
  6. ajax( "http://some.url.1", response );
  7. ajax( "http://some.url.2", response );

这里的并发“进程”是这两个用来处理 Ajax 响应的 response() 调用。它们可能以任意顺序运行。
我们假定期望的行为是 res[0] 中放调用 "http://some.url.1" 的结果,res[1] 中放调用 "http://some.url.2" 的结果。有时候可能是这样,但有时候却恰好相反,这要视哪个调用先完成而定。这种不确定性很有可能就是一个竞态条件 bug。
可以协调交互顺序来处理这样的竞态条件:

  1. var res = [];
  2. function response(data) {
  3. if (data.url == "http://some.url.1") {
  4. res[0] = data;
  5. }
  6. else if (data.url == "http://some.url.2") {
  7. res[1] = data;
  8. }
  9. }
  10. // ajax(..)是某个库中提供的某个Ajax函数
  11. ajax( "http://some.url.1", response );
  12. ajax( "http://some.url.2", response );

通过简单的协调,就避免了竞态条件引起的不确定性。
有些并发场景如果不做协调,就总是(并非偶尔)会出错:

  1. var a, b;
  2. function foo(x) {
  3. a = x * 2;
  4. baz(); }
  5. function bar(y) {
  6. b = y * 2;
  7. baz(); }
  8. function baz() {
  9. console.log(a + b);
  10. }
  11. // ajax(..)是某个库中的某个Ajax函数
  12. ajax( "http://some.url.1", foo );
  13. ajax( "http://some.url.2", bar );

无论 foo()bar() 哪一个先被触发,总会使 baz() 过早运行(a 或者 b 仍处于未定义状态);但对 baz() 的第二次调用就没有问题,因为这时候 ab 都已经可用了。
一种简单方法:

  1. var a, b;
  2. function foo(x) {
  3. a = x * 2;
  4. if (a && b) {
  5. baz();
  6. }
  7. }
  8. function bar(y) {
  9. b = y * 2;
  10. if (a && b) {
  11. baz();
  12. }
  13. }
  14. function baz() {
  15. console.log( a + b );
  16. }
  17. // ajax(..)是某个库中的某个Ajax函数
  18. ajax( "http://some.url.1", foo );
  19. ajax( "http://some.url.2", bar );

包裹baz()调用的条件判断if (a && b)传统上称为门(gate),虽然不能确定ab 到达的顺序,但是会等到它们两个都准备好再进一步打开门(调用 baz())。
另一种可能遇到的并发交互条件有时称为竞态(race),但是更精确的叫法是门闩(latch)。 它的特性可以描述为“只有第一名取胜”。在这里,不确定性是可以接受的,因为它明确指出了这一点是可以接受的:需要“竞争”到终点,且只有唯一的胜利者。

  1. var a;
  2. function foo(x) {
  3. a = x * 2;
  4. baz(); }
  5. function bar(x) {
  6. a = x / 2;
  7. baz(); }
  8. function baz() {
  9. console.log( a );
  10. }
  11. // ajax(..)是某个库中的某个Ajax函数
  12. ajax( "http://some.url.1", foo );
  13. ajax( "http://some.url.2", bar );

不管哪一个(foo()bar())后被触发,都不仅会覆盖另外一个给 a 赋的值,也会重复调用 baz()(很可能并不是想要的结果)。
可以通过一个简单的门闩协调这个交互过程,只让第一个通过:

  1. var a;
  2. function foo(x) {
  3. if (!a) {
  4. a = x * 2;
  5. baz(); }
  6. }
  7. function bar(x) {
  8. if (!a) {
  9. a = x / 2;
  10. baz(); }
  11. }
  12. function baz() {
  13. console.log( a );
  14. }
  15. // ajax(..)是某个库中的某个Ajax函数
  16. ajax( "http://some.url.1", foo );
  17. ajax( "http://some.url.2", bar );

条件判断if (!a)使得只有foo()bar()中的第一个可以通过,第二个(实际上是任何后续的)调用会被忽略。也就是说,第二名没有任何意义!

协作

还有一种并发合作方式,称为并发协作(cooperative concurrency)。这里的重点不再是通过 共享作用域中的值进行交互(尽管显然这也是允许的!)。这里的目标是取到一个长期运行的“进程”,并将其分割成多个步骤或多批任务,使得其他并发“进程”有机会将自己的运算插入到事件循环队列中交替运行。
举例:考虑一个需要遍历很长的结果列表进行值转换的 Ajax 响应处理函数。我们会使用 Array.prototype.map(..) 让代码更简洁:

  1. var res = [];
  2. // response(..)从Ajax调用中取得结果数组
  3. function response(data) {
  4. // 添加到已有的res数组
  5. res = res.concat(
  6. // 创建一个新的变换数组把所有data值加倍
  7. data.map( function(val){
  8. return val * 2;
  9. })
  10. );
  11. }
  12. // ajax(..)是某个库中提供的某个Ajax函数
  13. ajax( "http://some.url.1", response );
  14. ajax( "http://some.url.2", response );

如果 "http://some.url.1" 首先取得结果,那么整个列表会立刻映射到 res 中。如果记录有几千条或更少,这不算什么。但是如果有像 1000 万条记录的话,就可能需要运行相当一段时间了。这样的“进程”运行时,页面上的其他代码都不能运行,包括不能有其他的 response(..) 调用或 UI 刷新,甚至是像滚动、输入、按钮点击这样的用户事件。所以,要创建一个协作性更强更友好且不会霸占事件循环队列的并发系统,可以异步地批处理这些结果。每次处理之后返回事件循环,让其他等待事件有机会运行。

  1. var res = [];
  2. // response(..)从Ajax调用中取得结果数组
  3. function response(data) {
  4. // 一次处理1000个
  5. var chunk = data.splice( 0, 1000 );
  6. // 添加到已有的res组
  7. res = res.concat(
  8. // 创建一个新的数组把chunk中所有值加倍
  9. chunk.map( function(val){
  10. return val * 2;
  11. })
  12. );
  13. // 还有剩下的需要处理吗?
  14. if (data.length > 0) {
  15. // 异步调度下一次批处理
  16. setTimeout( function(){
  17. response( data );
  18. }, 0 );
  19. }
  20. }
  21. // ajax(..)是某个库中提供的某个Ajax函数
  22. ajax( "http://some.url.1", response );
  23. ajax( "http://some.url.2", response );

我们把数据集合放在最多包含 1000 条项目的块中。这样,就确保了“进程”运行时间会很短,即使这意味着需要更多的后续“进程”,因为事件循环队列的交替运行会提高站点 /App 的响应(性能)。
这里使用 setTimeout(..0)(hack)进行异步调度,基本上它的意思就是“把这个函数插入到当前事件循环队列的结尾处”。

严格说来,setTimeout(..0) 并不直接把项目插入到事件循环队列。定时器 会在有机会的时候插入事件。举例来说,两个连续的 setTimeout(..0) 调用 不能保证会严格按照调用顺序处理,所以各种情况都有可能出现,比如定时器漂移,在这种情况下,这些事件的顺序就不可预测。在 Node.js 中,类似的方法是 process.nextTick(..)。尽管它们使用方便(通常性能也更高), 但并没有(至少到目前为止)直接的方法可以适应所有环境来确保异步事件的顺序。