您的位置:首頁>科技>正文

認識電腦系統的非同步本質

圖靈機是一切現代電腦的基礎。 它是一種具有狀態的機器, 機器下一步要做什麼取決於當前的狀態(也包括紙條的位置)。 如果將當前的狀態清除, 程式將無法正常運行下去。 圖靈機的運行軌跡, 就如同一根針帶著一縷線, 在紙條上來回穿行。

這個模型被多種技術實現, 最終人類造出了基於半導體的CPU;而CPU指令集又經過多次抽象, 最終形成了高級程式設計語言;然而高級程式設計語言(例如BASIC或C語言)卻依然保留著圖靈機的影子——每一條語句執行完之後, 變數處於一定的狀態(擁有一定的值), 然後執行下一條語句, 下一條語句可能使用或者修改這個變數的值, 或者跳回之前的某條語句。 這種穿針引線的程式執行方式, 就是“同步”(synchronous), 也是絕大多數人從第一次接觸程式設計開始, 就根深蒂固的概念。

最早的時候, 電子電腦通過一定的外界信號控制,

從紙帶讀入並按照步驟執行某個程式。 一段時間過去, 雖然電腦的速度越來越快, 從外界獲取資訊的速度卻增長緩慢;向外界輸出資訊的速度(例如通過印表機)也十分有限。 到後來問題越發嚴重:電腦用於輸入輸出的時間比實際進行運算的時間還要長, 這是對資源的極大浪費。 個人電腦的普及也突出了這個問題:輸入命令所耗費的時間往往比執行命令的時間更長, 卻無法利用這些時間讓CPU處理其他任務。

最後大家得出結論:像圖靈機那樣一條線運行的電腦, 實在是太不好用了。

於是人們就希望電腦能夠同時運行多個程式。 為了同時運行多個程式, 科學家們想了個辦法:用一台圖靈機, 模擬多台圖靈機, 然後讓程式分別在多台虛擬的圖靈機上運行。

這些虛擬的圖靈機可以共用外部的輸入和輸出設備, 輸入輸出設備只有在接受或提交結果的時候, 才通過“中斷”信號通知電腦讀取或寫入。 因此在輸入輸出等非常耗時但不需要電腦參與的過程中, 電腦可以做其他事情。

於是就產生了可以並行運行大量程式的現代作業系統。

現代作業系統內核的主要工作, 就是虛擬許多個同時運行的【執行緒】, 在執行緒之間通過高速切換實現多工並行。 例如, 作業系統允許程式A所在的執行緒運行1毫秒, 然後將這個執行緒的所有“狀態”(所有寄存器的值)緩存起來, 再將程式B所在執行緒的“狀態”恢復到作業系統寄存器中, 繼續運行程式B。 這樣對於每一個獨立的程式來說, 其“狀態”是穩定的, 如同直接運行在獨立的CPU上一樣。 於是我們可以用“同步”的方式編寫程式, 讓作業系統幫我們解決“非同步”的問題。

為什麼叫“非同步”呢?因為從作業系統的角度來看, 沒有一個使用者程式是在CPU上按照指令一路執行到底的,

而是根據外部事件(比如上一個例子中的1毫秒計時器)動態切換的。 兩個程式同時運行, 也沒有辦法確定地指出哪個會先執行完。

其實這是大部分自然過程的本質;自然界裡沒有什麼事情會完美地按照某個確定的順序發生——同時在過程中不受環境影響——

最後只產生一個確定的結果。 所有的事情都以非同步的方式進行著, 畢竟大自然裡不存在像圖靈機這樣的理想機器。

在作業系統中, 每個【執行緒】的存在都會佔用一定的資源。 例如某個網路通信程式, 它的任務是組織10000個使用者聊天, 需要創建10000個TCP連接。 代碼很簡單:先接收使用者輸入, 然後根據一定規則轉發給其他使用者。 但如果將這段代碼編譯成程式, 在作業系統中創建10000個執行緒分別處理用戶輸入,會佔用GB級別的記憶體空間,而作業系統在10000個執行緒間來回切換也是一件很痛苦的事情。怎麼辦呢?

最常見的做法是,放棄作業系統執行緒帶來的便利(同步程式設計),在應用程式內部實現一個非同步的架構:由一個執行緒監聽所有使用者發來的聊天資料(使用者之間通過資料包頭區分),每當資料包到達時根據其中的使用者資訊,根據一定規則轉發給其他使用者。如果同時有兩個使用者發來資料包,則資料包會在網卡的緩衝區內排隊等待中的執行緒按順序讀取。這種程式設計方法,稱為非同步程式設計。通常,在應用程式內部實現非同步,比借用作業系統執行緒,性能上要高得多(沒有資源上的額外開銷)。因此,許多要求軟即時性、大規模併發、高可靠的電腦系統,其軟體都採用非同步/事件驅動/虛擬機器架構。

C語言的眾多函式程式庫多是為同步程式設計設計的。在非同步程式設計技術突飛猛進的今天,我們擁有了越來越多的選擇;其中Node.js在短短6年裡就成為了高性能網路服務器最重要的開發語言。它擁有接近於C語言的性能,以及閉包特性,最重要的是將非同步程式設計的基礎——事件佇列——變成了基礎設施,而不是僅僅提供一個函式程式庫。

然而非同步程式設計最大的問題在於,沒有辦法用同步的方式——也是大部分程式設計愛好者最喜愛的方式——撰寫含有非同步調用的程式。這成為了許多程式設計愛好者進步的瓶頸,聞非同步而色變。

Windows本身也是非同步的。。。大多數向系統註冊的回呼函數和STA執行緒模型的COM元件會在GetMessage()調用期間被調用,少數則會在DispatchMessage()期間被調用。

單執行緒非同步程式設計,是最簡單也應用最廣泛的非同步程式設計形式,一般是註冊個回呼函數或同步事件,然後將執行緒處於idle狀態(比如調用GetMessage/SleepEx/MsgWaitForSingleObject等),由系統在idle狀態調用回呼函數或觸發同步事件,使得程式繼續運行。使用這種非同步形式需要考慮的問題最少,只需要注意,一是代碼執行時間不要過長,二是回呼函數觸發順序不能預測。由於函數觸發順序不能預測,所以要注意狀態的傳遞問題。語言的閉包特性可以使得程式狀態被繼承,大大簡化了非同步程式設計。

多執行緒非同步程式設計,一般是註冊個回呼函數,然後由系統直接創建新執行緒執行。比如.NET中的System.Threading.Timer。使用這種非同步形式,不用擔心代碼執行時間過長的問題,但要考慮訪問公共資源的同步問題,不然程式可能會出現錯誤結果甚至崩潰。這種非同步形式對於系統資源的消耗也是比較大的。

中斷是另一種非同步程式設計,中斷可以在非idle狀態觸發,並插入尚未執行完的程式中執行。中斷非同步程式設計最麻煩,除了上面所說的問題之外,還要考慮狀態保護問題,不然正在執行的程式容易跑飛,甚至導致系統崩潰。大多數現代作業系統都將中斷機制封裝在ring0層,不允許ring3程式掛鉤中斷。

在JavaScript中非同步程式設計應用非常廣泛,是歷史中長期沒有多執行緒支持造成的。

在作業系統中創建10000個執行緒分別處理用戶輸入,會佔用GB級別的記憶體空間,而作業系統在10000個執行緒間來回切換也是一件很痛苦的事情。怎麼辦呢?

最常見的做法是,放棄作業系統執行緒帶來的便利(同步程式設計),在應用程式內部實現一個非同步的架構:由一個執行緒監聽所有使用者發來的聊天資料(使用者之間通過資料包頭區分),每當資料包到達時根據其中的使用者資訊,根據一定規則轉發給其他使用者。如果同時有兩個使用者發來資料包,則資料包會在網卡的緩衝區內排隊等待中的執行緒按順序讀取。這種程式設計方法,稱為非同步程式設計。通常,在應用程式內部實現非同步,比借用作業系統執行緒,性能上要高得多(沒有資源上的額外開銷)。因此,許多要求軟即時性、大規模併發、高可靠的電腦系統,其軟體都採用非同步/事件驅動/虛擬機器架構。

C語言的眾多函式程式庫多是為同步程式設計設計的。在非同步程式設計技術突飛猛進的今天,我們擁有了越來越多的選擇;其中Node.js在短短6年裡就成為了高性能網路服務器最重要的開發語言。它擁有接近於C語言的性能,以及閉包特性,最重要的是將非同步程式設計的基礎——事件佇列——變成了基礎設施,而不是僅僅提供一個函式程式庫。

然而非同步程式設計最大的問題在於,沒有辦法用同步的方式——也是大部分程式設計愛好者最喜愛的方式——撰寫含有非同步調用的程式。這成為了許多程式設計愛好者進步的瓶頸,聞非同步而色變。

Windows本身也是非同步的。。。大多數向系統註冊的回呼函數和STA執行緒模型的COM元件會在GetMessage()調用期間被調用,少數則會在DispatchMessage()期間被調用。

單執行緒非同步程式設計,是最簡單也應用最廣泛的非同步程式設計形式,一般是註冊個回呼函數或同步事件,然後將執行緒處於idle狀態(比如調用GetMessage/SleepEx/MsgWaitForSingleObject等),由系統在idle狀態調用回呼函數或觸發同步事件,使得程式繼續運行。使用這種非同步形式需要考慮的問題最少,只需要注意,一是代碼執行時間不要過長,二是回呼函數觸發順序不能預測。由於函數觸發順序不能預測,所以要注意狀態的傳遞問題。語言的閉包特性可以使得程式狀態被繼承,大大簡化了非同步程式設計。

多執行緒非同步程式設計,一般是註冊個回呼函數,然後由系統直接創建新執行緒執行。比如.NET中的System.Threading.Timer。使用這種非同步形式,不用擔心代碼執行時間過長的問題,但要考慮訪問公共資源的同步問題,不然程式可能會出現錯誤結果甚至崩潰。這種非同步形式對於系統資源的消耗也是比較大的。

中斷是另一種非同步程式設計,中斷可以在非idle狀態觸發,並插入尚未執行完的程式中執行。中斷非同步程式設計最麻煩,除了上面所說的問題之外,還要考慮狀態保護問題,不然正在執行的程式容易跑飛,甚至導致系統崩潰。大多數現代作業系統都將中斷機制封裝在ring0層,不允許ring3程式掛鉤中斷。

在JavaScript中非同步程式設計應用非常廣泛,是歷史中長期沒有多執行緒支持造成的。

Next Article
喜欢就按个赞吧!!!
点击关闭提示