(給ImportNew加星標,提高Java技能)
編譯:ImportNew/唐尤華
0. 引言
本文是多執行緒技術入門篇,對行程、執行緒、纖程、併發、並行、執行緒安全、競態條件等概念進行了介紹,討論了多執行緒技術的實現原理、使用中可能遇到的問題以及如何正確處理。
伴隨著硬體和作業系統的進步,現在的計算機能夠同時執行多個操作,程式執行更快響應時間縮短。
在軟體開發中使用併發既誘人又困難,需要瞭解計算機底層工作原理。本文是系列的第一篇,從作業系統中執行緒的基本概念入手,介紹執行緒背後的魔法。讓我們開始吧。
1. 正確認識行程與執行緒
現在的主流作業系統都支援同時執行多個程式,比如可以一邊在瀏覽器上看這篇文章一邊用播放器聽歌。執行中的瀏覽器和播放器程式都是**行程**,作業系統提供了一套機制利用底層硬體保證多個行程同時執行。無論具體採用的是哪種技術,最終讓你感覺這些程式是在同時執行。
在作業系統中同時執行多工,行程不是唯一選項。每個行程都能在內部併發執行子任務,子任務也被稱為執行緒。你可以把執行緒看成一個行程切片。行程啟動時會啟動至少一個執行緒,稱為主執行緒。接下來,根據程式或開發者的需要可以啟動新執行緒或終止執行緒。多執行緒即在一個行程中執行多個執行緒。
例如,播放器會執行多個執行緒。一個執行緒顯示介面,通常是主執行緒,另一個執行緒播放音樂。
可以把作業系統看成行程的容器,而每個行程又是執行緒的容器。雖然本文只關註執行緒,但這個主題非常吸引人,值得在未來深入分析。
“1. 作業系統這個盒子裡裝的是行程,行程又包含了一個或多個執行緒”
1.1 行程與執行緒的區別
作業系統會為每個行程分配一塊獨立的記憶體。預設情況下,一個行程的記憶體不能與其他行程共享。例如瀏覽器不能訪問播放器的記憶體,反之亦然。一個行程啟動的多個實體也是如此。啟動兩次瀏覽器,作業系統會把每個實體看作一個新行程,併為其分配一塊獨立的記憶體。因此,多個行程之間預設是無法共享資料的,除非使用行程間通訊(IPC)技術。
與行程不同,執行緒能夠共享父行程的記憶體。例如播放器中,音樂播放執行緒可以訪問介面執行緒的資料,反之亦然。因此,執行緒之間溝通起來更容易。此外,執行緒佔用資源更少,建立速度更快,這就是為什麼執行緒也被稱為輕量級行程。
如果沒有執行緒,需要將這些任務作為行程執行並透過作業系統同步。使用 IPC 通訊相對困難,而且由於行程比執行緒更“重”,執行速度也更慢。
1.2 Green Thread 纖程
到目前為止,執行緒都由作業系統管理,即必須經由作業系統才能啟動一個新執行緒,不過並非所有平臺都支援執行緒。Green Thread 也稱為纖程,它模擬執行緒的工作,可以在不支援本地執行緒的平臺上開發多執行緒應用。例如,如果作業系統不支援本地執行緒,虛擬機器會實現纖程。
纖程的優點是建立速度快、管理效率高,因為完全繞過了作業系統。但纖程也有缺點,會在下一篇中討論。
“Green Thread”代指 Sun 公司的 Green Team,他們在20世紀90年代設計了最初的 Java 執行緒庫。今天的 Java 不再使用纖程,早在2000年就已經開始使用本地執行緒了。一些其他程式語言,比如 Go、Haskell、Rust 等實現了類似纖程的機制代替本地執行緒。
2. 執行緒的作用
為什麼行程中需要使用多執行緒?正如之前提到的,並行可以加快處理速度。舉個例子,在影片編輯器裡渲染一部電影,編輯器會把渲染工作分配給多個執行緒,每個執行緒只處理其中一部分工作。假設一個執行緒需要1小時,那麼兩個執行緒只需要30分鐘,4個執行緒縮短到15分鐘,以此類推。
真的這麼簡單?還需要考慮以下三點:
- 不是所有程式都要使用多執行緒。如果程式本身順序執行或者頻繁等待使用者輸入,這種情況多執行緒無法發揮作用;
- 僅僅增加執行緒並不會讓程式跑得更快,每個子任務都需要經過仔細設計和考慮;
- 同樣,並不能100%保證並行,一切都依賴於底層硬體。
最後很關鍵的一點,如果計算機不支援同時執行多個操作,作業系統會讓它們看起來像同時執行,稍後會對此進行介紹。現在,讓我們把併發(concurrency)理解為多個任務看起來是並行執行的樣子,而並行(parallelism)是真正的同時執行。
“2. 並行是併發的子集”
3. 併發與並行的工作原理
CPU 是程式真正執行的地方,它由幾個部分組成,其中最主要的部分被稱為核心(core)。CPU 核心同時只能執行一個操作。
這是一個明顯的缺點。由於這個原因,作業系統設計了各種機制使圖形化環境即使在單核裝置上也可以支援多行程(多執行緒)。其中最重要的技術稱為搶佔式多工處理,這裡的“搶佔”指中斷當前任務切換到另一個任務,稍後再恢復前一個任務執行的能力。
如果 CPU 只有一個核心,那麼作業系統會將這個核心的計算能力分配給多個行程或執行緒,它們會在迴圈中一個接一個地執行。這種設計會造成任務在並行執行或者說程式同時在執行多個任務(多執行緒)的錯覺。**滿足了併發,但不是真正的並行**,並沒有同時執行多個行程。
如今的 CPU 已經不止一個核心,同一時刻每個核心都能獨立執行一次操作,這意味著對於多個核心的核心能夠真正做到並行。例如,我的 Intel Core i7 有4個核心,可以同時執行4個不同的行程或執行緒。
作業系統能夠檢測 CPU 核心的數量,併為每個核心分配行程或執行緒。只要作業系統喜歡,執行緒可以分配到任何核心上執行,這個過程對正在執行的程式而言完全透明。不僅如此,如果所有核心都在忙碌中,仍然可以啟用搶佔式多工機制。這樣可以執行更多的行程和執行緒,超過實際裝置的核心數量。
在單核 CPU 上使用多執行緒有意義嗎?
“單核無法做到真正的並行,但多執行緒程式設計是有意義的”。當行程包含了多個執行緒時,由於搶佔式多工機制,即使其中某個執行緒執行緩慢或任務阻塞,也可以保持程式執行。
舉個例子,執行的桌面程式正從磁碟讀資料,讀磁碟的過程非常緩慢。如果程式只有一個執行緒,那麼整個程式會出現“無法響應”直到讀取結束。CPU 所有計算資源都分配給了唯一的執行緒,浪費在等待磁碟IO完成上。當然,作業系統還會執行許多其他行程,但這個應用無法繼續響應。
讓我們用多執行緒方式設計這個應用。執行緒 A 訪問磁碟,與此同時執行緒 B 負責主介面。當 A 由於讀磁碟操作進入等待,執行緒 B 仍然可以保持介面能夠作出響應。由於這裡有兩個執行緒,作業系統可以在它們之間切換,不會在較慢的執行緒上卡住。
4. 執行緒越多,問題多多
正如我們知道的那樣,執行緒會共享父行程的記憶體,這使得應用中的多個執行緒可以更好地交換資料。例如,影片編輯器行程的記憶體中包含了時間軸資料,若干工作執行緒從記憶體讀取、渲染並把結果儲存到影片檔案中。它們需要一個記憶體控制代碼(例如指標)指向對應的記憶體區域,從而實現讀取並把渲染好的幀存入磁碟。
多個執行緒只從同一塊記憶體讀取沒有任何問題,但是當其中某個執行緒執行寫入而其它執行緒正在讀取時,問題來了。這時可能出現兩個問題:
- 資料競爭:讀執行緒正在讀記憶體,寫執行緒還沒有寫入完成,這時會讀到損壞的資料;
- 競態條件:讀執行緒應該只在寫執行緒完成操作後讀取,如果發生相反的情況會怎麼樣呢?比資料競爭更微妙的,競態條件可能是多個執行緒按照不可預知的順序執行操作,而實際的要求應該按照指定順序執行。因此,即使做到了資料保護,還是有可能觸發競態條件。
執行緒安全指什麼
一段程式碼如果聲稱自己執行緒安全,那麼應該做到在多執行緒呼叫時沒有資料競爭並且不會觸發競態條件。你可能會註意到一些開發庫聲稱自己是執行緒安全的,如果正在編寫多執行緒程式碼,那麼就需要確保所有第三方庫都能夠跨執行緒使用而且不會引起併發問題。
5. 資料競爭的根源
我們知道一個 CPU 核心一次只能執行一條機器指令,這種指令因為不可分割所以被稱為原子操作,即不能拆分成更小的操作。希臘單詞“atom”(ἄτομος; atomos)表示不可切分。
不可分割的特性讓原子操作天然執行緒安全。當一個執行緒寫入共享資料時,其他執行緒在修改完成前無法讀取。反過來,當一個執行緒讀共享資料操作具有原子性時,一定能夠讀到某個時刻的完整資料,不會出現某個執行緒進入原子操作中的情況,因此不會發生資料競爭。
壞訊息是絕大多數的操作都不是原子操作,即使 `x = 1` 這樣簡單的賦值陳述句在硬體上也可能由多個機器指令組成,所以說賦值陳述句本身不是執行緒安全的。因此如果一個執行緒讀取 `x`,而另一個執行緒執行賦值操作,就會產生資料競爭。
6. 競態條件的根源
搶佔式多工機制讓作業系統能完全控掌控執行緒管理:按照排程演演算法啟動、停止和暫停執行緒執行,程式員無法控制執行緒執行時間或執行順序。事實上,並不能保證下麵這段簡單的程式碼按照指定順序啟動:
```java
writer_thread.start()
reader_thread.start()
```
多執行幾次就能註意到,每次的執行的結果可能不一樣,有時寫執行緒先啟動,有時讀執行緒先啟動。如果要求程式確保在讀取之前進行寫入,那麼肯定會觸發競態條件。
這種行為被稱不非確定性:每次結果都會改變,無法預測。除錯帶有競態條件的程式非常麻煩,因為不能以可控的方式復現問題。
7. 教執行緒和諧相處:併發控制
資料競爭和競態條件都是現實中會出現的問題,有人甚至因此喪生。併發控制是一種多執行緒併發的藝術,作業系統和程式語言為此提供了一些解決方案,其中最重要的幾項有:
- 同步:確保一次僅有一個執行緒使用資源。同步是指將程式碼的特定部分標記為“protected”,這樣多個併發執行緒就不會同時執行這段程式碼,也不會讓共享資料變得混亂;
- 原子操作:由作業系統提供特殊指令,一些非原子操作,比如前面提到的賦值操作,可以轉化為原子操作。這樣,無論其他執行緒如何訪問,共享資料始終保持有效;
- 不可變資料:當共享資料被標記為不可變時,只允許執行緒從中讀取資料,沒有任何方式可以改變它的內容,從而解決了競態條件的根本原因。眾所周知,只要不修改記憶體地址,執行緒就可以安全地從相同的位置讀取資料。這也是函式式程式設計背後的哲學。
在系列的下一篇中,我們將討論這些有趣的併發話題,敬請期待!
8. 參考資料
- 8 bit avenue – [Difference between Multiprogramming, Multitasking, Multithreading and Multiprocessing 多執行緒、多工、多執行緒和多處理之間的區別][1]
- Wikipedia – [Inter-process communication 詞條][2]
- Wikipedia – [Process (computing) 詞條][3]
- Wikipedia – [Concurrency (computer science) 詞條][4]
- Wikipedia – [Parallel computing 詞條][4]
- Wikipedia – [Multithreading (computer architecture) 詞條][5]
- Stackoverflow – [Threads & Processes Vs MultiThreading & Multi-Core/MultiProcessor: How they are mapped? 執行緒與行程 vs 多執行緒與多核 vs 多處理器: 它們是如何對映的?][6]
- Stackoverflow – [Difference between core and processor? 核心與處理器的區別?][7]
- Wikipedia – [Thread (computing) 詞條][8]
- Wikipedia – [Computer multitasking 詞條][9]
- Ibm.com – [Benefits of threads][10]
- Haskell.org – [Parallelism vs. Concurrency 並行 vs 併發][11]
- Stackoverflow – [Can multithreading be implemented on a single processor system? 多執行緒可以在單處理器系統上實現嗎?][12]
- HowToGeek – [CPU Basics: Multiple CPUs, Cores, and Hyper-Threading Explained CPU基礎:多CPU、核心和超執行緒解釋][13]
- Oracle.com – [1.2 What is a Data Race? 什麼是資料競爭?][14]
- Jaka’s corner – [Data race and mutex 資料競爭與互斥][15]
- Wikipedia – [Thread safety 詞條][16]
- Preshing on Programming – [Atomic vs. Non-Atomic Operations 原子操作 vs 非原子操作][17]
- Wikipedia – [Green threads 詞條][18]
- Stackoverflow – [Why should I use a thread vs. using a process? 為什麼應該用執行緒而不是行程?][19]
- https://www.8bitavenue.com/difference-between-multiprogramming-multitasking-multithreading-and-multiprocessing/
- https://en.wikipedia.org/wiki/Inter-process_communication
- https://en.wikipedia.org/wiki/Process_%28computing%29
- https://en.wikipedia.org/wiki/Concurrency_%28computer_science%29
- https://en.wikipedia.org/wiki/Parallel_computing
- https://en.wikipedia.org/wiki/Multithreading_%28computer_architecture%29
- https://stackoverflow.com/questions/1713554/threads-processes-vs-multithreading-multicore-multiprocessor-how-they-are
- https://stackoverflow.com/questions/19225859/difference-between-core-and-processor
- https://en.wikipedia.org/wiki/Thread_%28computing%29
- https://en.wikipedia.org/wiki/Computer_multitasking
- https://www.ibm.com/support/knowledgecenter/en/ssw_aix_71/com.ibm.aix.genprogc/benefits_threads.htm
- https://wiki.haskell.org/Parallelism_vs._Concurrency
- https://stackoverflow.com/questions/16116952/can-multithreading-be-implemented-on-a-single-processor-system
- https://www.howtogeek.com/194756/cpu-basics-multiple-cpus-cores-and-hyper-threading-explained/
- https://docs.oracle.com/cd/E19205-01/820-0619/geojs/index.html
- http://jakascorner.com/blog/2016/01/data-races.html
- https://en.wikipedia.org/wiki/Thread_safety
- https://preshing.com/20130618/atomic-vs-non-atomic-operations/
- https://en.wikipedia.org/wiki/Green_threads
- https://stackoverflow.com/questions/617787/why-should-i-use-a-thread-vs-using-a-process
朋友會在“發現-看一看”看到你“在看”的內容