http://www.renfed.com/2017/05/21/js-threads/?utm_source=tuicool&utm_medium=referral
多線程對前端開發人員來說既熟悉又陌生,一方面前端幾乎很少寫多線程,另一方面多線程又經常會碰到,如你買個電腦它會标明它是四核八線程、四核四線程之類的,它是多核多線程的。什麼叫做多核呢?四核四線程和八線程又有什麼差別?
先來看一下自己電腦的CPU配置。
1. 檢視CPU配置
(1)自己電腦的配置
如在Mac上可以通過檢視系統偏好的方式,如下圖所示,有一個CPU,并且是四核的:

那怎麼看它是四線程還八線程呢?可以運作以下指令:
> sysctl hw.logicalcpu
hw.logicalcpu: 8
可以看到邏輯核數為8,是以它是八線程的,再來看一下CPU的型号:
> sysctl -n machdep.cpu.brand_string
Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz
(2)伺服器配置
如在Linux伺服器上面,可以運作以下指令:
> cat /proc/cpuinfo| grep “physical id”| sort| uniq
physical id: 0
physical id: 1
可以得知它有兩個實體CPU,也就是說這台伺服器插了兩個CPU。然後再看下每個CPU的實體核數:
> cat /proc/cpuinfo| grep “cpu cores”| uniq
cpu cores: 6
實體核為數6,總的邏輯核數為12:
> cat /proc/cpuinfo | grep “processor” | wc –l
12
也就是說每個CPU為六核六線程,總共有兩個CPU,是以是12核12線程。我們還可以看下它的記憶體:
> cat /proc/meminfo
MemTotal: 16322520 kB MemFree: 1065508kB
總記憶體為16G,目前可用記憶體為1G,并且這個資料是實時,同理上面CPU的資料也是實時,雖然它是cat了一個檔案。
2. 什麼是四核四線程?
一個CPU有幾個核它就可以跑多少個線程,四核四線程就說明這個CPU同一時間最多能夠運作4個線程,四核八線程是使用了超線程技術,使得單個核像有兩個核一樣,速度比四核四線程有所提升。但是當你看你電腦的任務管理器,你會發現實際上會運作幾千個線程,如下圖目前OS運作了1917個線程,376個程序(程序是線程的容器,每個程序至少有一個主線程)。
由于四核四線程的CPU同一時間隻能運作四個線程,是以有些線程會處于運作狀态,而大部份的線程會處理于中斷、堵塞、睡眠的狀态,是以這裡就涉及到作業系統的任務排程。
3. OS的任務排程
以Linux為例,先來看下Linux作業系統程序的分類:
(1)Linux程序分類
可分為三種:
a) 互動式程序
需要有大量的互動,如vi編輯器,大部分時間處于休眠狀态,但是要求響應要快
b)批處理程序
運作在背景,如編譯程式,需要占用大量的系統資源,但是可以慢點
c) 實時程序
需要立即響應并執行,如視訊播放器軟體,它的優先級最高
根據線程的優先級進行任務排程。
(2)任務排程方式
常用的有以下兩種:
a)SCHED_FIFO
實時程序或者說它的實時線程的優先級最高,先來先運作,直到執行完了,才執行下一個實時程序
b)SCHED_RR
對于普通線程使用時間片輪詢,每個線程配置設定一個時間片,目前線程用完這個時間片還沒執行完的,就排到目前優先級一樣的線程隊列的隊尾
雖然作業系統運作了這麼多個線程,但是它的CPU使用率還是比較低的,如下圖所示:
了解了多線程的概念後,我們可以來說JS的多線程web worker了
4. Webworker
HTML5引入了webworker,讓JS支援線程。我們用webworker做一個斐波那契計算,首先寫一個fibonacci函數,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 | function fibonacci ( num ) { if ( num <= 0 ) return 0 ; if ( num === 1 || num === 2 ) return 1 ; var fn = 1 , fn1 = 1 , fn2 = fn + fn1 ; for ( var i = 4 ; i <= num ; i ++ ) { fn = fn1 ; fn1 = fn2 ; fn2 = fn + fn1 ; } return fn2 ; } |
把這個函數寫到worker.js裡面,webworker有一個全局的函數叫onmessage,在這個回調裡面監聽接收主線程的資料:
1 2 3 4 5 6 7 8 | console . log ( "worker.js start" ) ; onmessage = function ( event ) { //主線程的資料通過event.data傳進來 var num = event . data ; var result = fibonacci ( num ) ; //計算完結果,給主線程發送一個消息 postMessage ( result ) ; } |
計算完結果後,再把結果postMessage給主線程。
主線程先啟動一個worker子線程,把資料發給它,同時監聽onmessage,取到子線程給它傳遞的計算結果,如下main.js:
1 2 3 4 5 6 7 8 9 | console . log ( "main.js start" ) ; var worker = new Worker ( "worker.js" ) ; worker . onmessage = function ( event ) { console . log ( "recieve result: " + event . data ) ; } ; var num = 1000 ; worker . postMessage ( num ) ; |
然後在頁面引入這個main.js的script就行了:
1 | <script src = "main.js" > </script> |
運作結果如下圖所示:
最後主線程列印出子線程計算的結果,可以看到JS如果發生了整型溢出會自動轉換成雙精度浮點數。
需要注意的是,JS的多線程是系統級别的。
5. JS的多線程是OS級别的
也就是說JS的多線程是真的多線程,如下,一口氣建立500個線程:
1 2 3 | for ( var i = 0 ; i < 500 ; i ++ ) { var worker = new Worker ( "worker.js" ) ; } |
然後觀察作業系統的線程數變化,如下圖所示:
你會發現作業系統一下子多了500個線程,也就是說JS的多線程是調的系統API建立的多線程。還有一種多線程是使用者級别的多線程,這種多線程并不會産生實際的系統線程,它是應用程式自已控制任務切換,如Ruby的Fiber。
我們一下子開了500個線程,有點任性,如果一下子開5000個呢?首先一個程序最多能有多少個線程,一般作業系統是有限制的,再者你開太多,Chrome會把你這個頁面殺了,如下,跑着跑着頁面就挂了:
假設WebWorker可以操作DOM。
6. 線程同步
由于web worker是不可以操作DOM的,那如果能夠操作DOM會發生什麼事情?必須要限制同一個DOM結點隻能有一個線程操作,不允許同個變量或者同一塊記憶體被同時寫入。
線程同步主要是靠鎖來實作,鎖可以分成三種。
(1)互斥鎖
如下代碼所示,假設有一個互斥鎖的類,叫Mutex:
1 2 3 4 5 6 7 8 9 10 11 12 | var mutext = new Mutext ( ) ; function changeHeight ( height ) { mutext . lock ( ) ; $ ( "#my-div" ) [ 0 ] . style . height = height + "px" ; mutext . unlock ( ) ; } //worker1 changeHeight ( 500 ) ; //worker2 changeHeight ( 600 ) ; |
在改變某個DOM元素的高度時,先把這塊代碼的執行給鎖住了,隻有執行完了才能釋放這把鎖,其它線程運作到這裡的時候也要去申請那把鎖,但是由于這把鎖沒有被釋放,是以它就堵塞在那裡了,隻有等到鎖被釋放了,它才能拿到這把鎖再繼續加鎖。
互斥鎖使用太多會導緻性能下降,因為線程堵塞在哪裡它要不斷地查那個鎖能不能用了,是以要占用CPU。
第二種鎖叫讀寫鎖。
(2)讀寫鎖
如下,假設讀寫鎖用ReadWriteRock表示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | var lock = new ReadWriteLock ( ) ; function changeHeight ( height ) { lock . lockForWrite ( ) ; $ ( "#my-div" ) [ 0 ] . style . height = height + "px" ; lock . unlock ( ) ; } function getHeight ( ) { //允許多個線程同時讀,但是不允許有一個線程進行寫 lock . lockForRead ( ) ; var height = $ ( "#my-div" ) [ 0 ] . style . height ; lock . unlock ( ) ; return height ; } |
在第二個函數getHeight擷取高度的時候可以給它加一個讀鎖,這樣其它線程如果想讀是可以同時讀的,但是不允許有一個線程進行寫入,如調第一個函數的線程将會堵塞。同理隻要有一個線程在寫,另外的線程就不能讀。
第三種鎖叫條件變量。
(3)條件變量
條件變量是為了解決生産者和消費者的問題,由用互斥鎖和讀寫鎖會導緻線程一直堵塞在那裡占用CPU,而使用信号通知的方式可以先讓堵塞的線程進入睡眠方式,等生産者生産出東西後通知消費者,喚醒它進行消費。
不同程式設計語言鎖的實作不一樣,但是總體上可以分為上面那三種。
7. 多線程操作DOM的問題
上面隻是做到了不允許多個線程同時執行:
$(“#my-div”)[0].style.height = height + “px”;
但是另外的函數也可以用選擇器去擷取那個DOM結點然後去設定它的高度,是以無法避免多線程同時寫的問題。
如果真的發生了同時寫的情況,那麼不僅僅是頁面崩了,而是整個浏覽器都崩了。是以這個就比較危險了,假設那邊我有一個頁面打了1萬個字還沒儲存,但是因為不小了打開了你的頁面,導緻整個浏覽器挂了還沒儲存就有點悲摧了。這裡有個問題,為什麼浏覽器會挂呢?因為作業系統檢測到異常,它要把你殺了,如果不把你殺了,它自己就要脆了,它一脆你也脆了。為什麼以前的windows系統會藍屏,因為它沒有檢測到應用程式的異常,任由應用程式胡作非為,結果為了保護硬體它必須得挂,它一挂應用程式也得跟着挂。
是以JS不給程式員犯錯的機會。
8. JS沒有線程同步的概念
JS的多線程無法操作DOM,沒有window對象,每個線程的資料都是獨立的。主線程傳給子線程的資料是通過拷貝複制,同樣地子線程給主線程的資料也是通過拷貝複制,而不是共享共一塊記憶體區域。
是以會web worker基本上出不了什麼錯。
然後我們再來看一下JS的單線程模型。
9. JS的單線程模型
如下圖所示,應該可以很清楚地表示:
在主邏輯裡面fun1和fun2的調用是連在一起的,它是一個執行單元,要麼還沒執行,要麼得一口氣執行完。執行完之後,再執行setTimout append到後面的。然後由于已經超過了setInterval定的20ms,是以又馬上執行setInterval的函數。這裡可以看出setTimeout的計時是從邏輯單元執行完了才開始計時,而setInterval是執行到這一行的時候就開時計時了。
單線程裡面有個特例:異步回調,異步回調是Chrome自己的IO線程處理的,每發一個請求必須要有一個線程跟着,Chrome限制了同一個域最多隻能發6個請求。
再來看下Chrome的多線程模型。
10. Chrome的多線程模型
每開一個tab,Chrome就會建立一個程序,程序是線程的容器,如下Chrome的任務管理器:
我們從click事件來看一下Chrome的線程模式是怎麼樣的,如下圖表所示:
首先使用者點選了滑鼠,浏覽器的UI線程收到之後,這個消息資料封裝成一個滑鼠事件發送給IO線程,IO線程再配置設定給具體頁面的渲染線程。其中IO線程和UI線程是浏覽器的線程,而渲染線程是每個頁面自己的線程。
如果在執行一段很耗時的JS代碼,渲染線程裡的render線程将會被堵塞,而main線程繼續接收IO線程發過來的消息并排隊,等待render線程處理。也就是說當頁面卡住的時候,不斷地點選滑鼠,等頁面空閑了,點選的事件會再繼續觸發。
這個是從浏覽器線程到頁面線程的過程,反過來從頁面線程到浏覽器線程的例子如下圖表所示(在代碼裡面改變光标形狀):
看完Chrome的,再來看Node.js的線程模型。
11. Node.js的單線程模型
我們知道Node.js也是單線程的,但是單線程如何處理高并發呢?傳統的web服務是多線程的,它們通常是先初始化一個線程池,來一個連接配接就從線程池裡取出一個空閑的線程處理,而用Node.js如果有一個連接配接處理時間過長,那麼其它請求将會被堵塞。
但是由于資料庫連接配接本來是就是多線程,調用作業系統的IO檔案讀取也是多線程,是以Node.js的異步是借助于資料庫和IO多線程。
這樣的好處是不需要啟動新的線程,不需要開辟新線程的空間,不需要進行線程的上下文切換,是以當服務應用不是計算類型的,使用Node.js可能反而會更快,同時由于是單線程的是以寫代碼更容易。缺點是不能夠提供很耗CPU的服務,如圖形渲染,複雜的算法計算等。
可以在同一台多核的伺服器上開多幾個Node服務彌補單線程的缺陷,然後用nginx均勻地分發請求。
我們出來轉了一圈之後,再回到webworker
12. 内聯webworker
當new很多個worker.js的時候,浏覽器會從緩存裡面取worker.js:
有時候并不想多管理一個JS檔案,想要把它搞内聯的。這個時候可以用HTML5的新的資料類型Blob,如下代碼所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | var blobURL = URL . createObjectURL ( new Blob ( [ '(' , function ( ) { function fibonacci ( ) { } onmessage = function ( oEvent ) { var num = event . data ; var result = fibonacci ( num ) ; postMessage ( result ) ; } } . toString ( ) , ')()' ] , { type : 'application/javascript' } ) ) ; var worker = new Worker ( blobURL ) ; worker . onmessage = function ( event ) { console . log ( ` recieve result : $ { event . data } ` ) } ; |
Blob還經常被用于分割大檔案。
最後,JS的設計是單線程,後來HTML5又引入webworker,它隻能用于計算,因為它不能改DOM,無法造成視覺上的效果。然後它不能共享記憶體,沒有線程同步的概念,是以可以認為JS還是單線程,可以把webworker當成另外的一種回調機制。
本文并不是要介紹webworker怎麼用,重點還是介紹一下多線程的一些概念,例如什麼叫多線程,它和CPU又有什麼關系,什麼叫線程同步,為什麼要進行線程同步,還讨論了JS/Chrome/Node的線程模型,相信看了本文對多線程應該會有更好的了解。