1.3 并行线程
术语“异步”和“并行”常常被混为一谈,但实际上它们的意义完全不同。记住,异步是关于现在 和将来 的时间间隙,而并行是关于能够同时发生的事情。
并行计算最常见的工具就是进程 和线程 。进程和线程独立运行,并可能同时运行:在不同的处理器,甚至不同的计算机上,但多个线程能够共享单个进程的内存。
与之相对的是,事件循环把自身的工作分成一个个任务并顺序执行,不允许对共享内存的并行访问和修改。通过分立线程中彼此合作的事件循环,并行和顺序执行可以共存。
并行线程的交替执行和异步事件的交替调度,其粒度是完全不同的。
举例来说:
function later() { answer = answer * 2; console.log( "Meaning of life:", answer ); }
尽管 later() 的所有内容被看作单独的一个事件循环队列表项,但如果考虑到这段代码是运行在一个线程中,实际上可能有很多个不同的底层运算。比如,answer = answer * 2 需要先加载 answer 的当前值,然后把 2 放到某处并执行乘法,取得结果之后保存回 answer 中。
在单线程环境中,线程队列中的这些项目是底层运算确实是无所谓的,因为线程本身不会被中断。但如果是在并行系统中,同一个程序中可能有两个不同的线程在运转,这时很可能就会得到不确定的结果。
考虑:
var a = 20; function foo() { a = a + 1; } function bar() { a = a * 2; } // ajax(..)是某个库中提供的某个Ajax函数 ajax( "http://some.url.1", foo ); ajax( "http://some.url.2", bar );
根据 JavaScript 的单线程运行特性,如果 foo() 运行在 bar() 之前,a 的结果是 42 ,而如果 bar() 运行在 foo() 之前的话,a 的结果就是 41 。
如果共享同一数据的 JavaScript 事件并行执行的话,那么问题就变得更加微妙了。考虑 foo() 和 bar() 中代码运行的线程分别执行的是以下两段伪代码任务,然后思考一下如果它们恰好同时运行的话会出现什么情况。
线程 1(X 和 Y 是临时内存地址):
foo(): a. 把a的值加载到X b. 把1保存在Y c. 执行X加Y,结果保存在X d. 把X的值保存在a
线程 2(X 和 Y 是临时内存地址):
bar(): a. 把a的值加载到X b. 把2保存在Y c. 执行X乘Y,结果保存在X d. 把X的值保存在a
现在,假设两个线程并行执行。你可能已经发现了这个程序的问题,是吧?它们在临时步骤中使用了共享的内存地址 X 和 Y 。
如果按照以下步骤执行,最终结果将会是什么样呢?
1a (把a的值加载到X ==> 20) 2a (把a的值加载到X ==> 20) 1b (把1保存在Y ==> 1) 2b (把2保存在Y ==> 2) 1c (执行X加Y,结果保存在X ==> 22) 1d (把X的值保存在a ==> 22) 2c (执行X乘Y,结果保存在X ==> 44) 2d (把X的值保存在a ==> 44)
a 的结果将是 44 。但如果按照以下顺序执行呢?
1a (把a的值加载到X ==> 20) 2a (把a的值加载到X ==> 20) 2b (把2保存在Y ==> 2) 1b (把1保存在Y ==> 1) 2c (执行X乘Y,结果保存在X ==> 20) 1c (执行X加Y,结果保存在X ==> 21) 1d (把X的值保存在a ==> 21) 2d (把X的值保存在a ==> 21)
a 的结果将是 21 。
所以,多线程编程是非常复杂的。因为如果不通过特殊的步骤来防止这种中断和交错运行的话,可能会得到出乎意料的、不确定的行为,通常这很让人头疼。
JavaScript 从不跨线程共享数据,这意味着不需要考虑这一层次的不确定性。但是这并不意味着 JavaScript 总是确定性的。回忆一下前面提到的,foo() 和 bar() 的相对顺序改变可能会导致不同结果(41 或 42 )。
可能目前还不是很明显,但并不是所有的不确定性都是有害的。这有时无关紧要,但有时又是要刻意追求的结果。关于这一点,本章和后面几章会给出更多示例。
完整运行
由于 JavaScript 的单线程特性,foo() (以及 bar() )中的代码具有原子性。也就是说,一旦 foo() 开始运行,它的所有代码都会在 bar() 中的任意代码运行之前完成,或者相反。这称为完整运行 (run-to-completion)特性。
实际上,如果 foo() 和 bar() 中的代码更长,完整运行的语义就会更加清晰,比如:
var a = 1; var b = 2; function foo() { a++; b = b * a; a = b + 3; } function bar() { b--; a = 8 + b; b = a * 2; } // ajax(..)是某个库中提供的某个Ajax函数 ajax( "http://some.url.1", foo ); ajax( "http://some.url.2", bar );
由于 foo() 不会被 bar() 中断,bar() 也不会被 foo() 中断,所以这个程序只有两个可能的输出,取决于这两个函数哪个先运行——如果存在多线程,且 foo() 和 bar() 中的语句可以交替运行的话,可能输出的数目将会增加不少!
块 1 是同步的(现在 运行),而块 2 和块 3 是异步的(将来 运行),也就是说,它们的运行在时间上是分隔的。
块 1:
var a = 1; var b = 2;
块 2(foo() ):
a++; b = b * a; a = b + 3;
块 3(bar() ):
b--; a = 8 + b; b = a * 2;
块 2 和块 3 哪个先运行都有可能,所以如下所示,这个程序有两个可能输出。
输出 1:
var a = 1; var b = 2; // foo() a++; b = b * a; a = b + 3; // bar() b--; a = 8 + b; b = a * 2; a; // 11 b; // 22
输出 2:
var a = 1; var b = 2; // bar() b--; a = 8 + b; b = a * 2; // foo() a++; b = b * a; a = b + 3; a; // 183 b; // 180
同一段代码有两个可能输出意味着还是存在不确定性!但是,这种不确定性是在函数(事件)顺序级别上,而不是多线程情况下的语句顺序级别(或者说,表达式运算顺序级别)。换句话说,这一确定性要高于多线程情况。
在 JavaScript 的特性 中,这种函数顺序的不确定性就是通常所说的竞态条件 (race condition),foo() 和 bar() 相互竞争,看谁先运行。具体来说,因为无法可靠预测 a 和 b 的最终结果,所以才是竞态条件。
如果 JavaScript 中的某个函数由于某种原因不具有完整运行特性,那么可能的结果就会多得多,对吧?实际上,ES6 就引入了这么一个东西(参见第 4 章),现在还不必为此操心,以后还会再探讨这一部分!
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论