執行緒同步 2:多工原理

本篇要來解釋電腦能夠同時進行許多程式的多工原理, 這對於我們進一步認識系統機制,以及了解後面介紹的各種執行緒機制相當有幫助!

現代的我們可以同時執行好幾個程式,一邊聽音樂、一邊上網追劇、一邊和好友網路聊天等等, 可以同時執行好多個程式。 就算是我們自己寫的程式,也可以要求啟動好多個執行緒分頭行事。 然而這種我們現在覺得司空見慣的電腦「多工」能力其實並不是與生俱來的。 曾經的早期電腦一次只能執行一個工作,放音樂的時候就是單純放音樂, 要打字寫文章的時候得先把音樂給停了才能做,反正任何工作都只能夠一次一個的進行, 直到多工機制被發明以前。

那麼電腦是如何讓多支程式能夠同時執行的呢? 有些小伙伴也許可能會喊出: 「我知道! 那是因為現在的 CPU 有好多個核心的緣故, 每一個 CPU 核心都可以執行一個程式,好多個核心就可以執行好多個程式了!」 這麼說好像也沒什麼問題,而且事實上多核心乃至多 CPU 確實可以增進多工的能力, 然而電腦(或者更確切的說是作業系統)卻並不是依靠多核心或多 CPU 來實現多工能力的! 比方說也就大約 20 年前的電腦 CPU 普遍不是多核的,都是單核的, 但是那時候的電腦仍然能夠同時執行成百上千的程式。 即便是多核 CPU 已經相當普及的現代,打開你的任務管理器, 數數看你有幾個 CPU 核心?一般現在通常大約是在 4 個到 32 個之間吧。 然後再數數看電腦後臺現在有多少支程式正在運作?以一般家用系統來說,掐指一算可能都有上百個後臺程式。 於是你會發現到一件事,如果多工的功能是依賴多個 CPU 的核心或者多個 CPU 裝置來實現的話, 那麼這多工的能力是遠遠無法應付這麼數量龐大的程式同時進行的! 那麼現代的電腦(作業系統)到底是如何實現多工機制的呢?

分時多工

timesharing 1core
Figure 1. 同步處理 3 個工作任務(Task)的執行示意

現代大部份的電腦作業系統採用的都是「分時多工」(Time-Sharing)的工作調度模式。 如上圖(Figure 1)以同時處理 3 個任務的情形為例, 每一個顏色磚塊代表一個工作,也就是一個 CPU 指令。 這裡的「指令」(Instruction)就是 CPU 能看得懂的指令碼,是真正能讓 CPU 知道該去幹什麼事的實際命令, 也就是我們的程式碼語言在經過編譯之後會變成的一連串二進位指令碼。 那麼一個工作任務(Task)就是一串指令碼的集合,而這的例子就有 3 個不同的任務要進行。 那麼我們的作業系統就會把這些指令拆成一段一段的, 先分出一小段時間去執行其中一個任務的指令 (這個一小段時間被稱為「時間片」(Timeslice),通常來說是大約幾個毫秒左右), 然後就暫停這個任務並切換去執行另一個任務一小段時間, 然後再切換執行另外一個任務,以此類推……。 就這樣,即便電腦可能只有一個 CPU 核心,也可以像影分身術那樣不斷在多個任務中切換, 雖然嚴格意義上來說在同一個時間裡 CPU 還是只能做一件事, 可是因為每一個任務一次執行的時間相當短暫,一般通常大約在數毫秒左右, 使得我們在感知上覺得好幾個任務是同時在進行的。 這就是分時多工的基本原理。

以上是單 CPU 核心,也就是同一個時間裡只有一個人(CPU)能做事情的情況, 但是若再進一步擴展到多核心乃至多 CPU,其實原理結構也還是一模一樣的, 只不過這時候在同一個時間裡,就真的是嚴格意義上的可以同時執行兩個任務了。 如同下圖(Figure 2)就是同樣 3 個任務只不過改由雙核心 CPU 來處理的情形示意, 然而再繼續擴展成更多核心數的話也是一樣的, 總之就還是同樣的多任務輪流切換執行的結構,只不過這次不是一個人(CPU), 而是兩個人可以同時分擔消化要執行的任務群。

timesharing 2cores
Figure 2. 使用雙核心處理多工任務時,可以真實的在同一個時間裡執行兩個任務(Task)

分時多工為什麼不會拖慢任務的執行?

解釋完分時多工的原理之後,不知有沒有讀者產生一個疑問: 「欸,分時執行多個任務好像也不能節省時間啊! 如果一個任務要一分鐘才能處理完成,三個任務那就要三分鐘。 雖然採用多工的方式可以同時雨露均沾,但是因為每一個任務變成一段時間才會被照顧一下, 所以雖然三個任務同時進行,但最後卻每個任務都要三分鐘才能完成,好像更耗時了是不是?」

是的沒錯,這個邏輯確實正確,而對於那些計算量滿載的程式來說也確實就是這樣, 實際上因為任務調度需要切換工作的關係(就是所謂的「上下文切換」(Context Switch)), 總體需要的執行時間還會比一個工作全部一口氣執行完了再換下一個的做法還要更久。 然而當下流行的這種多工手段之所以在實際應用上可以同時處理多個程式任務還不多消耗時間, 根本的奇妙原因其實是因為大部份的程式的絕大部份時間裡都在閒著等待! 什麼意思呢? 比方以你的程式裡面要讀取檔案內容來說,讀取檔案這件事對 CPU 來說是屬於相當曠日費時的事情, 當下達了讀取的命令之後要等到實際將資料送回來都是猴年馬月之後的事了(以 CPU 視角來看), 而這段時間 CPU 就只能空等著。 類似這樣的程式操作還有很多,比如網路收送資料、等待週邊設備收送訊息等等, 甚至對於大部份的圖形介面(GUI)程式來說, 程式的執行週期裡其實花最多的時間是在等待你按下按鈕等操作輸入,而真正幹實事的時間比例實際上相當低。 因此我們實際每天使用的大部份程式的任務工作裡其實包含了大量的等待, 而在分時多工的任務調派結構下,作業系統就會在一個任務進入等待的時候立刻切換去執行別的任務。 這就是為什麼我們可以同時開啟多個程式,甚至如果打開任務管理器的話可能還能見到上百個程式在背景默默的執行著, 卻讓我們並不覺得程式會卡住變慢的原因了! (不過你不要問我 Windows,我也不知道為什麼 Windows 有些時候放著啥事不幹也會卡頓,那是微軟的問題!)

然而以上是對於大部份的日常程式而言,但若對於那些計算集中類型的程式, 比方跑模擬計算、影片與特效渲染、壓縮與解壓縮等等的這些程式來說,那麼情況就不一樣了! 對於這種任務裡多是密集的計算工作的,而非日常程式含有大量等待的程式, 它們確實就會發生向前面所提問的問題一樣的情況。 當這種計算密集的任務數量小於等於 CPU 核心數量的時候,執行基本不受影響, 因為多個 CPU 核心真實的可以同時執行這些任務; 然而當計算密集的任務數量大於 CPU 核心數量的時候,延遲與卡頓情況就出現了。 這時多工系統雖然也能看起來同時執行這些任務,但是總共需要完成的時間就更久了, 甚至於會排擠到其它處理視窗、鍵盤等等這類工作的任務, 從而導致使用者能夠體感到電腦的操作反應變得遲緩。

多工的背後

感覺上好像多工處理原理就是這麼的簡單,不過就是這邊做一做就放著,然後再到那邊做一做, 就是把工作切成一小段一小段然後輪流執行。 原理就是這樣簡單是沒錯的,然而實際上的多工排程其實是相當複雜的。 怎麼說呢? 假設我們的電腦同時有 100 條任務要進行好了,那麼每個任務應該要平均分配時間嗎? 或者換個話說,每個任務是地位相等的嗎? 有沒有一些任務可能比其它任務更加重要,使得我們希望作業系統儘量在比例上多分配一些時間片給它們? 又或者有沒有哪一些任務可能相當具有即時性,使我們並不樂見它們在某些事情發生的時候還要排隊排好久才輪到執行? 我們是否希望某些高時效性的任務能在需要的時候可以插隊搶佔? 然而這樣又會產生出更多問題來,比方說如果我的電腦執行了不少被設為高重要性、高時效性的任務, 那麼有沒有可能作業系統和 CPU 光處理那些任務就忙死了,完全沒空處理等級較低的任務? 有些任務可能重要性真的很低,例如即時通訊軟體接收好友傳來的訊息, 但是你覺得在你跑一些影片渲染、計算模型等工作的時候,朋友正在瘋狂敲你的訊息完全收不到是可以接受的嗎? 意思就是在滿是高優先級任務滿載的情況之下, 是否其它那些即便是最不重要的任務,任務排程也應該偶爾照顧到它? 或者是也許用戶覺得高優先級的任務最重要,讓低優先級的餓死也沒關係(作者我還真處理過這樣設計的系統)? 或甚至於,作業系統分配的時間片(Timeslice)應該是等長的嗎?

提出以上這些問題並不是真的要我們去做解答,因為實際上這些問題的解答都是好幾本論文的份量了! 將這些問題提問出來,只是想讓我們知道原來在簡單原理的背後還有這麼多的門道。 而在作業系統內去完成這些任務分配與調度的工作,就叫作任務「排程」(Scheduling)。 排程的各種算法和取捨也是一個相當有挑戰性的工作, 事實上對於那些世界上頂尖的超級電腦,往往開發設計團隊還會針對它們的需求去調整優化自己的任務排程器的設計; 好在對於我們站在應用程式的視角來說,不需要去實際處理這些問題, 只需要知道原來我們的執行緒(就是本文裡的任務(Task))在作業系統背後經歷了這些事情, 然後知道我們的執行緒無論如何會在合理的時間裡被作業系統喚醒執行就可以了!

最後再來補充一些關於時間片與任務切換的時機。 原則上作業系統排程器會在適當的時候給予任務一段執行工作的時間,也就是前面解釋過的時間片, 正常來說當這個時間終止以後,排程器就會把任務暫停,然後切換去執行其它的任務。 然而其實在許多實際的情況下這個時間片可能是用不完的,也就是時間片的時間還沒到,任務就會被切走了。 什麼時候會發生這種事呢? 除了可能被那些重要性比你更高的任務插隊,或者被硬體或軟體的「中斷」(Interrupt)機制打斷之外, 其實很多時候是任務自己主動放棄的! 比方說當我們的程式讀取檔案內容的時候、呼叫網路收送的時候、呼叫系統安排調度資源的時候等等, 那些操作的後面就是要進入等待,等到該等的東西來了以後才能夠繼續往下執行。 因此這時候即便時間片的時間還沒有用完,我們的任務也會立刻被作業系統給岔出去 (然後直到等到了我們的程式需要的東西之前都不會回來了)。

總結

本篇解釋了現代分時多工的原理,了解了電腦系統如何讓我們的電腦可以同步執行成千上萬個程式與執行緒的方式, 其實就是在各個工作之間不斷來回切換,這邊做一小段再去那邊做一小段。 並且本篇也在解釋敘述的過程中介紹了許多與執行緒和排程相關的專有名詞, 了解這些名詞與意義將有助於在讀者自行搜索相關資料的時候更容易理解。 本篇的原理解釋也是本系列文章裡相當重要的一部份,將是系列後續其它文章的背景知識基礎。

上一篇:「執行緒同步 1:簡介」
下一篇:「執行緒同步 3:任務、執行緒、行程、與程式」