關于同步和異步,我們先來看兩個例子。
const async=()=>{
console.log('async..',1)
let t=new Date();
while (true){
if(+new Date()-t>=2000){
console.log('async...',2)
break
}
}
console.log('async...',3)
}
async();
//async.. 1
//async... 2
//async... 3
順序執行
const sync=()=>{
console.log('sync...',1);
setTimeout(()=>{
console.log('sync...',2)
},2000);
console.log('sync...',3)
}
sync();
//sync... 1
//sync... 3
//sync... 2
可能都知道JavaScript是單線程的,即同一時刻只能做一件事,如果有多個任務,則需要排隊執行,但是這樣同步執行的效率低,如果一個任務長時間據有CPU,其他任務則需要等待,這無疑會浪費資源,造成資源利用率低。為此,
JavaScript將任務的執行分為兩種:
- 同步:后一個任務等待前一個任務結束,然后再執行,程序的執行順序與任務的排列順序是一致的、同步的;
- 異步:后一個任務不等前一個任務結束就執行,程序的執行順序與任務的排列順序是不一致的、異步的
需要了解的基本知識
進程與線程
進程(process):我們知道,程序運行是需要系統資源的(CPU,內存,I/O等)為了能使程序能夠并發執行,并對并發執行的程序加以描述和控制,從而引入進程。是進程實體的運行過程,是系統進行資源分配和調動的獨立單位
線程(thread):通過引入線程,一個能獨立運行的基本單位,作為操作系統調度和分派的基本單位。通過減少程序在并發進行時所付出的時空開銷,從而提高程序并發執行的程度,以及提高資源利用率和系統的吞吐量
進程和線程的區別和關系:
- 進程是操作系統分配資源的最小單位,線程是程序執行的最小單位。
- 一個進程由一個或多個線程組成,線程是一個進程中代碼的不同執行路線;
- 進程之間相互獨立,但同一進程下的各個線程之間共享程序的內存空間(包括代碼段、數據集、堆等)及一些進程級的資源(如打開文件和信號)。
- 調度和切換:線程上下文切換比進程上下文切換要快得多。
以工廠模式比喻,江南皮革廠老板苦惱產量提不上去,于是給車間主任(進程)開會。
老板:你們幾個怎么搞得,怎么訂單(多任務)多了,產量還跟不上了?
車間主任A:手底下這么多人,哪管的過來。訂單不好分配、進度不無法跟蹤
車間主任B:A車間經常跟我們車間搶物料(系統資源)
車間主任A:放***屁,你們的人上個月還搶我設備呢(系統資源)
老板:當然你們說的都是客觀存在的因素,你們不會從手下挑選幾個有能力的人成立班組(線程)么?權利下放(獨立調度)
車間主任A,B:好好好,試試。
linux下查看進程的常用命令
- ps
- top
Javascript 單線程
在ecma-262中并無與線程相關的內容,其單線程/多線程主要依賴于其JavaScript引擎(解釋器)。
瀏覽器的進程和線程
以Chrome為例,Chrome瀏覽器使用多個進程來隔離不同的網頁。因此在Chrome中打開一個網頁相當于起了一個進程
多進程的原因:
- 因為進程之間相互獨立,一個瀏覽器選項卡無響應不會影響其他的選項卡。
- 同樣因為進程之間相互獨立,一個選項卡中如果惡意腳本,不會影響其他選項卡。例如,Chrome 瀏覽器可以對處理用戶輸入(如渲染器)的進程,限制其文件訪問的權限。
GUI線程:渲染布局(HTML,CSS等)
JS引擎線程:1.解析、執行JS 2.與GUI線程互斥 (因為引擎是單線程的,所以Javascript是單線程的)
定時觸發器線程:setTimeout/setInterval
事件觸發線程:將滿足觸發條件的時間放入任務隊列
異步HTTP請求線程:XHR所在線程
單線程的原因(引用自瀏覽器進程?線程?傻傻分不清楚!)
這是因為Javascript這門腳本語言誕生的使命所致:JavaScript為處理頁面中用戶的交互,以及操作DOM樹、CSS樣式樹來給用戶呈現一份動態而豐富的交互體驗和服務器邏輯的交互處理。如果JavaScript是多線程的方式來操作這些UI DOM,則可能出現UI操作的沖突; 如果Javascript是多線程的話,在多線程的交互下,處于UI中的DOM節點就可能成為一個臨界資源,假設存在兩個線程同時操作一個DOM,一個負責修改一個負責刪除,那么這個時候就需要瀏覽器來裁決如何生效哪個線程的執行結果。當然我們可以通過鎖來解決上面的問題。但為了避免因為引入了鎖而帶來更大的復雜性,Javascript在最初就選擇了單線程執行。
Node.js中的進程和線程
當一個 Node.js 的應用啟動的同時,它會啟動如下模塊:
- 一個進程
- 一個線程:單線程意味著在當前進程中同一時刻只有一個指令在執行。
- 事件循環機制:盡管 JavaScript 是單線程的,但通過使用回調,promises, async/await 等語法,基于事件循環將對操作系統的操作異步化,使得 Node 擁有異步非阻塞 IO 的特性。
- JS 引擎實例
- Node.js 實例
Node 運行在單線程上,并且在事件循環中同一時刻只有一個進程的任務被執行,每次同一時刻只會執行一段代碼(多段代碼不會同時執行)。
只有一個js引擎在主線程上運行。其他異步IO和事件驅動相關的線程通過libuv來實現內部的線程池和線程調度
為什么Node.js也是單線程呢?
因為JavaScript起初是運行在瀏覽器端的...你懂的。
Node.js如何實現并行:
它通過事件輪詢(event loop)來實現并行操作,因此我們要避免阻塞操作,
Node.js可以多進程/多線程么?
利用
child_process
模塊 fork子進程,實現多進程利用
cluster
模塊fork子進程--Master-Worker,實現多進程
- 使用第三方包node-threads-a-gogo 實現多線程
前端的異步場景
- 定時器
- 網絡請求
- 事件綁定
- ES6 Promise
定時器
再回到我們的代碼
console.log('sync...',1);
setTimeout(()=>{
console.log('sync...',2)
},2000);
console.log('sync...',3)
流程分析:
- 主程序調用棧
- 調用webAPI-
setTimeout
- 定時器線程計數2s
- 事件觸發線程將定時器時間放入任務隊列
- 主線程通過Event Loop遍歷任務隊列執行異步任務
console.log()
console.time('test')
const test=()=>{
let t=+new Date();
while (true){
if(+new Date()-t>=5000){
break;
}
}
setTimeout(()=>{
console.log('setTimeout...')
},2000)
}
test();
console.timeEnd('test');
注意:
- 定時任務可能不會按時執行,延遲原因(等待同步任務、等待CPU加載)
- 定時器嵌套5次之后最小間隔不能低于4ms
應用場景:
- 防抖
- 節流
- 倒計時
- 動畫(存在丟幀)
案例分析
for(var i=1;i<=10;i++){
setTimeout(function() {
console.log(i)
},1000*i)
}
在這個案例中,我們期望每隔1s,依次打印i的值。但實際結果卻是每隔1s重復打印11。
問題的原因在于:
- 定時器需要等待同步任務執行完成,那么i的值為11。
-
var
是全局作用域
修復方案:
//使用閉包,保留當前的作用域
for(var i=1;i<=10;i++){
(i=>{
setTimeout(function() {
console.log(i)
},1000*i)
})(i)
}
//使用let 保留當前的作用域
for(let i=1;i<=10;i++){
setTimeout(function() {
console.log(i)
},1000*i)
}