- 原文地址:A gentle introduction to multithreading
- 原文做者:Triangles
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:steinliber
- 校對者:Graywd,Endone
現代計算機已經具有了在同一時間執行多個操做的能力。在更先進的硬件和更智能的操做系統支持下,這個特徵可讓你程序的執行和響應速度變得更快。html
編寫可以利用這種特性的軟件會頗有意思,但也很棘手:這須要你理解計算機背後所發生的事情。在第一節中,我將會試着簡單覆蓋關於線程的知識,它是由操做系統提供能實現這種魔術的工具之一。讓咱們開始吧!前端
現代操做系統能夠在同一時間運行多個程序。這就是爲何你能夠在瀏覽器(一個程序)閱讀這篇文章的同時還能夠在播放器(另外一個程序)上收聽音樂。這裏的每一個程序被認爲是一個正在執行的進程。操做系統知道不少軟件層面的技巧來使一個進程和其餘進程一塊兒運行,也能夠利用底層硬件來實現這個目的。不管哪一種方式,最終的結果就是你會感受全部程序都正在同時運行。android
在操做系統中運行進程並非同時執行多個操做惟一的方式。每一個進程其內部還能夠同時運行多個子任務,這些子任務叫作線程。你能夠把線程理解爲進程自己的一部分。每一個進程在啓動時至少會觸發一個線程,被稱爲主線程。而後,根據程序/開發者的須要,能夠在進程內啓動和終止額外的線程。多線程就是指在同一個進程中運行多個線程的技術。ios
好比說,你的播放器就可能運行了多個線程:一個線程用來渲染界面 —— 這個線程一般是主線程,另外一個用於播放音樂等等。git
你能夠把操做系統理解爲一個包含多個進程的容器,其中的每一個進程都是一個包含多個線程的容器。在本文中,我將只關注線程,可是這整個主題都很吸引人,因此值得在未來作更深刻的分析。github
圖1:操做系統能夠被看做一個包含進程的盒子,進程又能夠被看做包含一個或多個線程的盒子。算法
每一個進程都有屬於它本身的內存塊,由操做系統負責進行分配。在默認狀況下,進程之間不能共享彼此的內存塊:瀏覽器程序沒法訪問分配給播放器的內存,反之亦然。就算你運行了相同的進程實例(好比你啓動了瀏覽器兩次),它們之間也不會共享內存。操做系統將每一個實例視爲一個新的進程,並分配其各自獨立的內存。因此,在通常狀況下,多個進程相互之間沒法共享數據,除非它們使用一些高級的技巧 —— 所謂的進程間通訊。編程
和進程不同,線程共享由操做系統分配給其父進程的同一塊內存:這樣播放器的音頻引擎能夠很簡單的讀取到主界面的數據,反之亦然。所以相較於進程,線程之間相互通訊更加容易。除此以外,線程一般比進程更輕:它們佔用的資源更少,建立的速度更快,這就是爲何它們也被稱爲輕量級進程的緣由。後端
要讓你的程序在同一時間執行多個操做,線程是一種簡單的方式。若是沒有線程,你就須要爲每一個任務寫一個程序,把它們做爲進程運行並經過操做系統對這些進程進行同步。相較之下,這不只會變得更難(進程間通訊比較棘手)並且速度更慢(進程比線程更重)。瀏覽器
到目前爲止提到的線程都是操做系統層面的概念:一個進程想要啓動一個新線程必須經過操做系統。然而並不是每一個平臺都原生支持線程。綠色線程,也被稱爲纖程是對線程的一種模擬,使多線程程序能夠在不提供線程能力的環境下工做。好比說,在虛擬機的底層操做系統並無對線程原生支持的狀況下,它仍是能夠實現綠色線程。
綠色線程能夠更快的建立和管理,由於對其的操做徹底繞過了操做系統,可是這也有缺點。我將在下一節中談到這個話題。
「綠色線程」的名字來自於 Sun Microsystem 的綠色團隊,他們在 90 年代設計了 Java 最初 的線程庫。如今,Java 再也不使用綠色線程:它們在 2000 年的時候被切換成了原生線程。其它一些像 Go,Haskell 或者 Ruby 等編程語言 —— 它們採用了和綠色線程相同的實現而沒有用原生線程。
爲何一個進程應該使用多個線程?就像我以前提到的,並行處理能夠極大加快速度。假設你要在電影編輯器中渲染一部電影。這個編輯器足夠智能的話,它能夠將渲染操做分散到多個線程中,每一個線程負責處理電影的一部分。這樣的話若是用一個線程處理該任務要一個小時,那麼使用兩個線程則須要 30 分鐘;使用 4 個線程要 15 分鐘,以此類推。
真的有那麼簡單嗎?這裏有三點須要考慮:
最後相當重要的一點:若是你的計算機不支持在同一時間執行多個操做,操做系統就會假裝成它們是那樣運行的。咱們以後將會立刻看到這個。目前,讓咱們把併發理解成咱們看起來任務在同時運行,而真正的並行就是像字面上理解的那樣,任務在同一時間運行。
圖 2:並行是併發的子集。
計算機的中央處理單元(CPU)負責運行程序的繁重工做。它由幾部分組成,其中主要的部分叫作核心:這就是實際執行計算的地方。一個核心在同一時間只能執行一個操做。
無疑,這是核心一個主要的缺點。所以,操做系統層面提供了先進的技術使用戶可以同時運行多個進程(或線程),特別是在圖形環境中,甚至在單核機器上。其中最重要的方式叫作搶佔式多任務處理,這裏面的搶佔式是指能夠控制中斷正在運行的任務,切換到另外一個任務,一段時間後再恢復執行以前運行任務的能力。
所以若是你的 CPU 只有一個核心,那麼操做系統的一部分工做就是把這個單核的計算能力分配到多個進程或線程中,這些進程或線程會一個接一個地循環執行。這種操做會給你一種多個程序在並行運行的錯覺,若是是使用了多線程,就會以爲這個程序在同時作不少事。這知足了併發性,可是並非真的並行 —— 即同時運行進程的能力仍然是缺失的。
目前現代 CPU 都會有多個核心,其中每一個核心同一時間執行一次獨立的操做。這意味着在多核的狀況下真正的並行是能夠實現的。好比說,個人 Intel Core i7 處理器有 4 個核心:它能夠同時運行 4 個不一樣的進程和線程。
操做系統能夠檢測 CPU 內部核心的數量併爲其中的每個都分配進程或者線程。只要操做系統喜歡,線程能夠被分配到其中的任何一個核心,而且這種調度對於運行的程序來說是徹底透明的。另外若是全部核心都在忙的話,搶佔式多任務就會參與其中進行調度。這就可讓你可以運行比計算機實際可用核心數量更多的進程和線程。
在單核機器上是不可能實現真正意義上的並行的。然而,若是你的應用能夠從多線程中獲益,那在單核機器上跑多線程應用仍是有意義的。這種狀況下當一個進程使用多線程的時候,即便其中的一個線程在執行比較慢或者阻塞的任務,搶佔式多任務機制仍是可讓應用保持運行。
好比說你正在開發一個桌面應用,它會從一個很慢的磁盤讀取一些數據。若是你只是寫了個單線程程序,整個應用在讀取數據的時候就會失去響應一直到讀取完成:分配給這個惟一線程的 CPU 算力在等待磁盤喚醒的過程當中被浪費。固然,操做系統還運行了除此以外的其它不少進程,可是你這個特定應用的運行將不會有任何進展。
讓咱們從新用多線程的方式思考你的應用。程序的線程 A 負責磁盤訪問,線程 B 負責主界面。若是線程 A 因爲設備讀取慢而卡住,線程 B 仍運行着主界面,從而讓你的應用保持響應。這是有可能的,由於有了兩個線程,操做系統就能夠在它們之間切換分配 CPU 資源,而不會讓這個程序由於較慢的線程而卡住。
如咱們所知,線程共享它們父進程的同一塊內存。這使得在同一個應用的線程間交換數據很是容易。好比:一個電影編輯器可能有一大部分的共享內存用於包含視頻時間線。這樣的共享內存被數個用於渲染電影到文件中的工做線程讀取。它們只須要一個指向該內存區域的句柄(例如指針),就能夠從中讀取數據並將渲染幀輸出到磁盤。
只要多個線程是從同一個內存位置讀取數據那這事情還算順利。若是它們之中的一個或多個寫數據到共享內存中而有其餘線程正從中讀取數據的時候,麻煩就開始了。這個時候會出現兩個問題:
數據競爭 —— 當寫線程修改內存的時候,讀線程可能這在讀這個內存。若是寫線程尚未完成寫操做,讀線程將會獲得損壞的數據;
競爭條件 —— 讀線程應該在寫線程寫完以後才能讀內存。若是事情發生的順序正好相反呢?比數據競爭更微妙在於,競爭條件是指多個線程以不可預知的順序執行它們的工做,而實際上,咱們想要這些操做按照正確的順序執行。即便對數據競爭作了保護,你的程序可能仍是會觸發競爭條件。
若是一段代碼由多個線程同時執行,且正常工做,即沒有數據競爭或競爭條件,那麼就能夠說它是線程安全的。你可能已經注意到一些程序庫聲明本身是線程安全的:若是你正在編寫一個多線程程序,想要確保任何第三方的函數能夠跨線程使用而不會觸發併發問題,就要注意這些聲明。
咱們知道一個 CPU 核心在同一時間只能執行一條機器指令。這樣的指令叫作原子操做由於它是不可分割的:它不能被分解成更小的操做。希臘語單詞 「atom」(ἄτομος; atomos)就是指不能被切分了。
不可分割的屬性使原子操做本質上就是線程安全的。當一個線程在共享數據上執行原子寫時,沒有其它線程能夠讀取被修改了一半的數據。相反,當一個線程在共享數據上執行原子讀時,它會讀取在某一時刻出如今內存中的整個值。在執行原子操做的時候其它線程不可能矇混過關插入進來,所以就不會發生數據競爭。
不幸的是,絕大部分操做都是非原子的。在一些硬件上即便是像 x = 1
這樣簡單的賦值操做也多是由多個原子機器指令組成的,這就使賦值操做這個總體自己成爲一個非原子操做。若是一個線程在讀取 x
值的同時另外一個線程在對其進行賦值就會觸發數據競爭。
搶佔式多任務機制給予了操做系統對線程管理徹底的控制權:它能夠根據高級調度算法來開始,中止或者暫停線程。做爲開發者,你不能控制線程執行的時間或者順序。實際上,像下面這樣簡單的代碼也不能保證按照特定的順序啓動:
writer_thread.start()
reader_thread.start()
複製代碼
運行這個程序幾回,你就會注意到它每次運行的行爲是如何的不一樣:有時寫線程先啓動,有時讀線程先啓動。若是你的程序須要在讀以前先寫,那麼確定會遇到競爭條件。
這種表現被稱爲非肯定性:運行結果每次都會改變而你沒法預測。調試受競爭條件影響的程序很是煩人,由於你不能老是以一種可控的方式來重現問題。
數據競爭和競爭條件都是現實世界的問題:有些人甚至因之而死。調度多個併發線程的藝術叫作併發控制:爲了處理這個問題,操做系統和編程語言提供了幾個解決方案。其中最重要的是:
同步 —— 一種確保同一時間資源只會被一個線程使用的方式。同步就是把代碼的特定部分標記爲「受保護的」,這樣多個併發線程就不會同時執行這段代碼,避免它們把共享數據搞砸;
原子操做 —— 因爲操做系統提供了特殊指令,許多非原子操做(像以前的賦值操做)能夠變成原子操做。這樣,不管其它線程如何訪問共享數據,共享數據始終保持有效狀態。
不可變數據 —— 共享數據被標記爲不可變的,沒有什麼能夠改變它:線程只能從中讀取,這樣就消除了根本緣由。正如咱們所知,只要不修改內存線程就能夠安全的從相同的內存位置讀取數據。這是函數式編程背後的主要理念。
在這個關於併發的小系列下一節中,我將會討論全部這些引人入勝的主題。敬請期待!
8 bit avenue - Difference between Multiprogramming, Multitasking, Multithreading and Multiprocessing
Wikipedia - Inter-process communication
Wikipedia - Process (computing)
Wikipedia - Concurrency (computer science)
Wikipedia - Parallel computing
Wikipedia - Multithreading (computer architecture)
Stackoverflow - Threads & Processes Vs MultiThreading & Multi-Core/MultiProcessor: How they are mapped?
Stackoverflow - Difference between core and processor?
Wikipedia - Thread (computing)
Wikipedia - Computer multitasking
Ibm.com - Benefits of threads
Haskell.org - Parallelism vs. Concurrency
Stackoverflow - Can multithreading be implemented on a single processor system?
HowToGeek - CPU Basics: Multiple CPUs, Cores, and Hyper-Threading Explained
Oracle.com - 1.2 What is a Data Race?
Jaka's corner - Data race and mutex
Wikipedia - Thread safety
Preshing on Programming - Atomic vs. Non-Atomic Operations
Wikipedia - Green threads
Stackoverflow - Why should I use a thread vs. using a process?
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。