天天看點

JS與多線程

http://www.renfed.com/2017/05/21/js-threads/?utm_source=tuicool&utm_medium=referral

多線程對前端開發人員來說既熟悉又陌生,一方面前端幾乎很少寫多線程,另一方面多線程又經常會碰到,如你買個電腦它會标明它是四核八線程、四核四線程之類的,它是多核多線程的。什麼叫做多核呢?四核四線程和八線程又有什麼差別?

先來看一下自己電腦的CPU配置。

1. 檢視CPU配置

(1)自己電腦的配置

如在Mac上可以通過檢視系統偏好的方式,如下圖所示,有一個CPU,并且是四核的:

JS與多線程

那怎麼看它是四線程還八線程呢?可以運作以下指令:

> 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個程序(程序是線程的容器,每個程序至少有一個主線程)。

JS與多線程

由于四核四線程的CPU同一時間隻能運作四個線程,是以有些線程會處于運作狀态,而大部份的線程會處理于中斷、堵塞、睡眠的狀态,是以這裡就涉及到作業系統的任務排程。

3. OS的任務排程

以Linux為例,先來看下Linux作業系統程序的分類:

(1)Linux程序分類

可分為三種:

a) 互動式程序

需要有大量的互動,如vi編輯器,大部分時間處于休眠狀态,但是要求響應要快

b)批處理程序

運作在背景,如編譯程式,需要占用大量的系統資源,但是可以慢點

c) 實時程序

需要立即響應并執行,如視訊播放器軟體,它的優先級最高

根據線程的優先級進行任務排程。

(2)任務排程方式

常用的有以下兩種:

a)SCHED_FIFO

實時程序或者說它的實時線程的優先級最高,先來先運作,直到執行完了,才執行下一個實時程序

b)SCHED_RR

對于普通線程使用時間片輪詢,每個線程配置設定一個時間片,目前線程用完這個時間片還沒執行完的,就排到目前優先級一樣的線程隊列的隊尾

雖然作業系統運作了這麼多個線程,但是它的CPU使用率還是比較低的,如下圖所示:

JS與多線程

了解了多線程的概念後,我們可以來說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如果發生了整型溢出會自動轉換成雙精度浮點數。

需要注意的是,JS的多線程是系統級别的。

5. JS的多線程是OS級别的

也就是說JS的多線程是真的多線程,如下,一口氣建立500個線程:

1 2 3 for ( var i = 0 ; i < 500 ; i ++ ) {      var worker = new Worker ( "worker.js" ) ; }

然後觀察作業系統的線程數變化,如下圖所示:

JS與多線程

你會發現作業系統一下子多了500個線程,也就是說JS的多線程是調的系統API建立的多線程。還有一種多線程是使用者級别的多線程,這種多線程并不會産生實際的系統線程,它是應用程式自已控制任務切換,如Ruby的Fiber。

我們一下子開了500個線程,有點任性,如果一下子開5000個呢?首先一個程序最多能有多少個線程,一般作業系統是有限制的,再者你開太多,Chrome會把你這個頁面殺了,如下,跑着跑着頁面就挂了:

JS與多線程

假設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的單線程模型

如下圖所示,應該可以很清楚地表示:

JS與多線程

在主邏輯裡面fun1和fun2的調用是連在一起的,它是一個執行單元,要麼還沒執行,要麼得一口氣執行完。執行完之後,再執行setTimout append到後面的。然後由于已經超過了setInterval定的20ms,是以又馬上執行setInterval的函數。這裡可以看出setTimeout的計時是從邏輯單元執行完了才開始計時,而setInterval是執行到這一行的時候就開時計時了。

單線程裡面有個特例:異步回調,異步回調是Chrome自己的IO線程處理的,每發一個請求必須要有一個線程跟着,Chrome限制了同一個域最多隻能發6個請求。

再來看下Chrome的多線程模型。

10. Chrome的多線程模型

每開一個tab,Chrome就會建立一個程序,程序是線程的容器,如下Chrome的任務管理器:

JS與多線程

我們從click事件來看一下Chrome的線程模式是怎麼樣的,如下圖表所示:

JS與多線程

首先使用者點選了滑鼠,浏覽器的UI線程收到之後,這個消息資料封裝成一個滑鼠事件發送給IO線程,IO線程再配置設定給具體頁面的渲染線程。其中IO線程和UI線程是浏覽器的線程,而渲染線程是每個頁面自己的線程。

如果在執行一段很耗時的JS代碼,渲染線程裡的render線程将會被堵塞,而main線程繼續接收IO線程發過來的消息并排隊,等待render線程處理。也就是說當頁面卡住的時候,不斷地點選滑鼠,等頁面空閑了,點選的事件會再繼續觸發。

這個是從浏覽器線程到頁面線程的過程,反過來從頁面線程到浏覽器線程的例子如下圖表所示(在代碼裡面改變光标形狀):

JS與多線程

看完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與多線程

有時候并不想多管理一個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的線程模型,相信看了本文對多線程應該會有更好的了解。

繼續閱讀