javascript之單線程

javascript的單線程機制


爲什麼javascript採用單線程機制?

        先說明一下線程和進程的不同:進程是執行的應用程序,每個進程都是由私有的虛擬地址空間、代碼、數據和其它系統資源所組成,進程在運行過程中能夠申請創建和使用系統資源,這些資源也會隨着進程的終止而被銷燬。線程是進程內的一個獨立執行單元,在不同的線程之間是可以共享進程資源的,所以在多線程的情況下,需要特別注意對臨界資源的訪問控制。在系統創建進程之後就開始啓動執行進程的主線程,而進程的生命週期和這個主線程的生命週期一致,主線程的退出也就意味着進程的終止和銷燬。主線程有系統進程所創建,同時用戶也可以自己創建其他線程,這一系列的線程都會併發地運行與同一個進程中。


        多線程的併發,能夠提高CPU利用率,提升應用程序性能。但是javascript中卻沒有使用多線程,而是使用了單線程。而其中的原因,要從Javascript的用途說起,javascript是一種瀏覽器腳本語言,主要實現與用戶的交互,操作DOM。如果使用多線程的方式操作DOM,則可能出現操作的衝突。假設有兩個線程同時操作一個DOM元素,線程1要求瀏覽器刪除DOM,線程2要求修改DOM樣式,瀏覽器是無法決定採用哪個線程的操作的。爲了簡化開發,javascript支持的是單線程。

        

        單線程運行,即在某一時刻內只能執行特定的一個任務,只有當這個任務完全執行完畢後,才能執行下一個任務。即會發生阻塞的現象。比如如果有I/O輸入的情況,而I/O輸入過程比較耗時,在單線程中,那麼程序會阻塞在I/O輸入模塊上,只有輸入完成後,纔會執行之後的程序。這樣就會造成CPU很長一段時間都是空閒的。所以需要引入異步實現任務的功能。而javascript具有回調的特性(注意,回調和單線程並不是一回事,不要混淆,單線程只是很好地應用了回調這個特性),對於比較耗時的I/O輸入,不需要等待輸入的完成再執行之後的代碼,而是可以先執行後面的代碼,等這些耗時的任務完成後則以回調的方式執行相應的處理,這個過程很完美地體現了回調機制。


Runtime概念


下面給出的是一個理論上的模型,現在js引擎已着重實現和優化了以下所描述的幾個概念



Stack(棧)

這裏放着JavaScript正在執行的任務。每個任務被稱爲幀(stack of frames)。

function f(b){
  var a = 12;
  return a+b+35;
}

function g(x){
  var m = 4;
  return f(m*x);
}

g(21);
上述代碼調用 g 時,創建棧的第一幀,該幀包含了 g 的參數和局部變量。當 g 調用 f 時,第二幀就會被創建,並且置於第一幀之上,當然,該幀也包含了 f 的參數和局部變量。當 f 返回時,其對應的幀就會出棧。同理,當 g 返回時,棧就爲空了(棧的特定就是後進先出 Last-in first-out (LIFO))。

Heap(堆)

一個用來表示內存中一大片非結構化區域的名字,對象都被分配在這。

Queue(隊列)

一個 JavaScript runtime 包含了一個任務隊列,該隊列是由一系列待處理的任務組成。而每個任務都有相對應的函數。當棧爲空時,就會從任務隊列中取出一個任務,並處理之。該處理會調用與該任務相關聯的一系列函數(因此會創建一個初始棧幀)。當該任務處理完畢後,棧就會再次爲空。(Queue的特點是先進先出 First-in First-out (FIFO))。

爲了方便描述與理解,作出以下約定:

  • Stack棧爲主線程
  • Queue隊列爲任務隊列(等待調度到主線程執行)

那麼,現在具體講一下主線程和任務隊列


主線程和任務隊列


        單線程就意味着,所有任務需要排隊,前一個任務結束,纔會執行後一個任務。如果前一個任務耗時很長,後一個任務就不得不一直等着。 

        如果排隊是因爲計算量大,CPU忙不過來,倒也算了,但是很多時候CPU是閒着的,因爲IO設備(輸入輸出設備)很慢(比如Ajax操作從網絡讀取數據),不得不等着結果出來,再往下執行。


        JavaScript語言的設計者意識到,這時主線程完全可以不管IO設備,掛起處於等待中的任務,先運行排在後面的任務。等到IO設備返回了結果,再回過頭,把掛起的任務繼續執行下去。 
於是,所有任務可以分成兩種,一種是同步任務(synchronous),另一種是異步任務(asynchronous)。同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;異步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務可以執行了,該任務纔會進入主線程執行。 
        具體來說,異步執行的運行機制如下。(同步執行也是如此,因爲它可以被視爲沒有異步任務的異步執行。) 
(1)所有同步任務都在主線程上執行,形成一個執行棧(execution context stack)。 
(2)主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。 
(3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,於是結束等待狀態,進入執行棧,開始執行。 
(4)主線程不斷重複上面的第三步。 

事件和回調函數 


        "任務隊列"是一個事件的隊列(也可以理解成消息的隊列),IO設備完成一項任務,就在"任務隊列"中添加一個事件,表示相關的異步任務可以進入"執行棧"了。主線程讀取"任務隊列",就是讀取裏面有哪些事件。 
"任務隊列"中的事件,除了IO設備的事件以外,還包括一些用戶產生的事件(比如鼠標點擊、頁面滾動等等)。只要指定過回調函數,這些事件發生時就會進入"任務隊列",等待主線程讀取。 
所謂"回調函數"(callback),就是那些會被主線程掛起來的代碼。異步任務必須指定回調函數,當主線程開始執行異步任務,就是執行對應的回調函數。 

        "任務隊列"是一個先進先出的數據結構,排在前面的事件,優先被主線程讀取。主線程的讀取過程基本上是自動的,只要執行棧一清空,"任務隊列"上第一位的事件就自動進入主線程。但是,由於存在後文提到的"定時器"功能,主線程首先要檢查一下執行時間,某些事件只有到了規定的時間,


Event Loop機制

        之所以被稱爲Event loop,是因爲它以以下類似方式實現:

while(queue.waitForMessage()){
  queue.processNextMessage();
}
        正如上述所說,“任務隊列”是一個事件的隊列,如果I/O設備完成任務或用戶觸發事件(該事件指定了回調函數),那麼相關事件處理函數就會進入“任務隊列”,當主線程空閒時,就會調度“任務隊列”裏第一個待處理任務,(FIFO)。當然,對於定時器,當到達其指定時間時,纔會把相應任務插到“任務隊列”尾部。

        上圖主線程的綠色部分,還是表示運行時間,而橙色部分表示空閒時間。每當遇到I/O的時候,主線程就讓Event Loop線程去通知相應的I/O程序,然後接着往後運行,所以不存在紅色的等待時間。等到I/O程序完成操作,Event Loop線程再把結果返回主線程。主線程就調用事先設定的回調函數,完成整個任務。


定時器問題

        在任務隊列中,也可以存放定時器。在js中,定時器函數有兩個setTimeout()和setInterval()。

        setTimeout()方法在指定的時間過後執行代碼,需要傳遞兩個參數,第一個參數是一個函數(函數代碼塊或者是函數名),第二個參數是指等待的毫秒數。即在第二個參數指定的時間後執行第一個指定的函數內容。但是這裏要注意的是,javascript是一個單線程程序的解釋器,定時器是被當做異步任務來處理的,所以setTimeout()的第二個參數告訴javascript再過多長時間把當前任務(第一個參數指定的任務)添加到隊列中即使是setTimeout中第二個參數設置爲0,也不會立馬執行)。只有等到主線程的任務執行完,並且任務對列是空的,纔會執行setTimeout指定的代碼任務。

        setTimeout()方法,在有些時候並不會按照我們設置的超時時間來執行指定代碼,但是通過調用setTimeout方法,改變了代碼流程,將主線程中的代碼執行完畢,纔會執行指定的代碼。

setInterval()方法,每隔指定的時間就執行一次代碼。需要傳遞兩個參數,第一個參數爲要執行的代碼,第二個參數爲每次執行前需要等待的毫秒數。


參考資料

關於javascript單線程的一些事

單線程運行機制及setTimeout(fn,0)

單線程模型

Javascript是單線程的深入分析




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章