原文: https://github.com/getify/You-Dont-Know-JS/blob/master/async%20%26%20performance/ch1.md#parallel-threading
譯者:熊賢仁
并行線程
人們常常把 “異步” 和 “并行” 這兩個術(shù)語混淆,但它們其實(shí)完全不同虽另。記住暂刘,異步是關(guān)于 “現(xiàn)在” 和 “將來” 之間的間隙,而并行說的是能夠同時發(fā)生的事情洲赵。
進(jìn)程和線程是并行計算最常用的工具鸳惯。進(jìn)程和線程獨(dú)立運(yùn)行,也可能同時運(yùn)行:在多個獨(dú)立的處理器上叠萍,或者多個獨(dú)立的計算機(jī)上芝发,而多線程可以在同一個進(jìn)程上共享內(nèi)存。
相比之下苛谷,事件循環(huán)將工作分成多個任務(wù)辅鲸,并串行執(zhí)行它們,不允許對共享內(nèi)存做并行訪問和改變腹殿。通過獨(dú)立線程中的相互協(xié)作的事件循環(huán)独悴,并行化和 “串行化” 可以共存。
并行線程的交錯執(zhí)行和異步事件的交錯執(zhí)行锣尉,其顆粒度是完全不同的刻炒。
比如:
function later() {
answer = answer * 2;
console.log( "Meaning of life:", answer );
}
雖然 later()
的全部內(nèi)容會被作為一個事件循環(huán)隊列的入口,然而考慮這段代碼運(yùn)行在一個線程上自沧,實(shí)際上可能有許多不同的底層操作坟奥。比如,answer = answer * 2
要先加載 answer
的當(dāng)前值拇厢,接著把 2
放在某處爱谁,然后做乘法計算,然后取出結(jié)果孝偎,并把結(jié)果存回 answer
中访敌。
在單線程環(huán)境下,線程隊列中的這些項目是底層操作其實(shí)是無所謂的衣盾,因?yàn)?br> 線程不能被中斷寺旺。但如果你有一個并行系統(tǒng),其中兩個不同的線程在同一個程序中運(yùn)行势决,你很可能得到意想不到的結(jié)果迅涮。
考慮:
var a = 20;
function foo() {
a = a + 1;
}
function bar() {
a = a * 2;
}
// ajax(..) is some arbitrary Ajax function given by a library
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
鑒于 JavaScript 的單線程特性,如果 foo()
在 bar()
之前運(yùn)行徽龟, a
計算的結(jié)果為 42
叮姑,但如果 bar()
在 foo()
之前運(yùn)行,a
的結(jié)果將是 41
据悔。
如果共享相同數(shù)據(jù)的 JS 事件并行執(zhí)行传透,那么問題就更加微妙了〖牵考慮下面兩列偽碼朱盐, foo()
和 bar()
代碼運(yùn)行時所在的線程執(zhí)行的是下面兩列偽碼,思考他們正好同一時刻運(yùn)行會發(fā)生什么菠隆。
線程 1 (X
和 Y
是臨時內(nèi)存地址)
foo():
a. load value of `a` in `X`
b. store `1` in `Y`
c. add `X` and `Y`, store result in `X`
d. store value of `X` in `a`
線程 2 (X
和 Y
是臨時內(nèi)存地址)
bar():
a. load value of `a` in `X`
b. store `2` in `Y`
c. multiply `X` and `Y`, store result in `X`
d. store value of `X` in `a`
現(xiàn)在兵琳,讓我們假設(shè)這兩個線程真的在并行運(yùn)行狂秘。你可能會發(fā)現(xiàn)問題所在,對嗎躯肌?它們在臨時步驟中使用了共享的內(nèi)存地址 X
和 Y
者春。
如果上述步驟是這樣的, a
的最終結(jié)果又是什么呢清女?
1a (load value of `a` in `X` ==> `20`)
2a (load value of `a` in `X` ==> `20`)
1b (store `1` in `Y` ==> `1`)
2b (store `2` in `Y` ==> `2`)
1c (add `X` and `Y`, store result in `X` ==> `22`)
1d (store value of `X` in `a` ==> `22`)
2c (multiply `X` and `Y`, store result in `X` ==> `44`)
2d (store value of `X` in `a` ==> `44`)
a
的結(jié)果將是 44钱烟。那這樣排列呢?
1a (load value of `a` in `X` ==> `20`)
2a (load value of `a` in `X` ==> `20`)
2b (store `2` in `Y` ==> `2`)
1b (store `1` in `Y` ==> `1`)
2c (multiply `X` and `Y`, store result in `X` ==> `20`)
1c (add `X` and `Y`, store result in `X` ==> `21`)
1d (store value of `X` in `a` ==> `21`)
2d (store value of `X` in `a` ==> `21`)
a
的結(jié)果將是 21嫡丙。
所以拴袭,多線程編程是相當(dāng)復(fù)雜的,因?yàn)槿绻悴徊扇√厥獠襟E去防止此類沖突和交錯的發(fā)生的話曙博,你會的到非常意外的不確定的結(jié)果拥刻,這常常讓人頭痛不已。
JavaScript 從不跨線程共享數(shù)據(jù)父泳,這意味著我們不需要擔(dān)心上述的不確定性泰佳。但也不意味著 JS 總是確定的域仇。還記得前面提到的嗎压储?foo()
和 bar()
的相對排列順序?qū)е铝瞬煌慕Y(jié)果(41 或 42)。
注意: 可能至今還不明顯窿凤,但不是所有的不確定性都是有害的睬捶。它有時無關(guān)緊要黔宛,有時是我們刻意而為的。通過本章節(jié)的接下來的一些章節(jié)擒贸,我們將看到更多的這方面的例子臀晃。
完整運(yùn)行
由于 JavaScript 的單線程特性,foo()
和 bar()
內(nèi)部的代碼是原子的介劫,這意味著一旦 foo()
開始運(yùn)行徽惋,必須要等到這段代碼全部執(zhí)行完畢, bar()
中的代碼才能開始運(yùn)行座韵,反之亦然险绘。這被稱為 “完整運(yùn)行” 行為。
事實(shí)上誉碴,如果 foo()
和 bar()
包含更多的代碼宦棺,完整運(yùn)行的語義會更清晰,比如:
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(..) is some arbitrary Ajax function given by a library
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
因?yàn)?foo()
不能被 bar()
中斷黔帕,bar()
也不能被 foo()
中斷代咸,所以這個程序只有兩種可能的輸出,這取決于哪個先運(yùn)行——如果存在多線程成黄,而且 foo()
和 bar()
中的語句可以交替運(yùn)行的話呐芥,可能輸出結(jié)果的數(shù)量將大大增加逻杖!
塊 1 是同步的(當(dāng)前執(zhí)行),但 塊 2 和 3 是異步的(將來執(zhí)行)思瘟,這意味著他們的執(zhí)行在時間上是分割的荸百。
塊 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 的執(zhí)行順序不確定,所以這段程序有兩個可能的輸出潮太,如下所示:
輸出 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
同一段代碼有兩種輸出管搪,這意味著仍然存在著不確定性虾攻!但這種不確定性是函數(shù)(事件)順序級別的铡买,而不是多線程下的語句順序級別(或者說,其實(shí)是表達(dá)式執(zhí)行順序級別)霎箍。換句話說奇钞,這比多線程還是要確定的多。
在 JavaScript 的特性中漂坏,這種函數(shù)順序不確定性就是通常所說的 “競態(tài)條件”景埃,因?yàn)?foo() 和 bar() 在相互競爭,來看看誰先運(yùn)行顶别。具體來說谷徙,正因?yàn)闊o法可靠預(yù)測 a 和 b 的計算結(jié)果,所以它才是 “競態(tài)條件”驯绎。
注意:如果 JS 中有個函數(shù)完慧,它無論如何也不具備完全執(zhí)行的特性,我們會有更多種可能的輸出剩失,對吧屈尼?ES6 就引入了這樣一個東西(見第四章 “Generators”),但現(xiàn)在別擔(dān)心拴孤,我們以后會介紹這一部分的脾歧!
本系列下一部分將介紹 “并發(fā)”