并發與并行是不同的,這一事實常常被忽視或誤解。 在許多開發人員之間的對話中,這兩個術語經常互換使用,意思是“與其他東西同時運行的東西”。有時在這種情況下使用“并行”這個詞是正確的,但通常如果開發人員正在討論代碼, 他們真的應該使用“并發”這個詞。
并發和并行之間的差異導致在建模代碼時,演變成非常顯著的抽象區別,Go充分利用了這一點。 讓我們來看看這兩個概念是如何不同的,以便我們能夠理解這種抽象的力量。 我們將從一個非常簡單的陳述開始:
>并發是代碼的一個屬性; 并行是正在運行的程序的一個屬性。
這是一個有趣的區別。我們編寫我們的代碼,以便它可以并行執行。 對?
那么,我們再來考慮一下。 如果所編寫的代碼的意圖是兩個程序塊并行運行,那么當程序運行時,我是否有任何保證? 如果我在只有一個內核的機器上運行代碼會發生什么? 你們中有些人可能會想,它會并行運行,但事實并非如此。
程序塊可能表現為并行運行,但實際上他們是以一種連續的方式執行,而不是不可區分的。(單核)CPU上下文切換為在不同程序之間共享時間,在足夠長的時間間隔內,這些任務表現為并行運行。如果我們要在兩個內核的機器上運行相同的二進制文件,那么程序塊可能確實是并行運行的。
這揭示了一些有趣且重要的事情。我們的代碼可能不是并行的,而表現出來卻有可能是并行的。并行是我們程序運行時的一個屬性,而非代碼。
第二個有趣的地方在于,我們發現運行時不知道我們的并發代碼是否實際并行運行。對程序模型的抽象可以使我們區分并發和并行,并最終賦予程序力量和靈活性。
第三,并行性是時間或環境的函數。我們在之前的“原子性”中討論了上下文的概念。在那里,上下文被定義為一個操作被認為是原子的邊界。 在這里,它被定義為兩個或多個操作可以被認為是并行的邊界。
例如,如果我們的上下文是五秒鐘的空間,并且我們運行了兩個每秒需要運行一次的操作,那么我們會認為這些操作是并行運行的。 如果我們的情況是一秒鐘,我們會認為這些操作是按順序運行的。
就時間片而言,重新定義我們的上下文可能并不是很好,但記住上下文不受時間限制。 我們可以將上下文定義為程序運行的過程,操作系統線程或其機器。 這很重要,因為定義的上下文與并發性和正確性的概念密切相關。 就像原子操作根據上下文可以被視為原子操作一樣,根據定義的上下文,并發操作是正確的。當然 這都是相對的。
這有點抽象,所以我們來看一個例子。 假設我們正在討論的環境是你的電腦。 除了理論物理外,我們可以合理地預期在我的機器上執行的進程不會影響機器上進程的邏輯。 如果我們都啟動計算器過程并開始執行一些簡單的算術運算,那么我執行的計算不應該影響你執行的計算。
這個例子有點傻。但是如果我們把它分解,我們會看到所有的部分在起作用:我們的機器是上下文,進程是并發操作。 在這種情況下,我們選擇通過獨立的計算機,操作系統和流程來思考世界并行操作。 這些抽象使我們能夠確認這一的思考是正確的。
>使用單獨的計算機似乎是一個有意義的例子,但個人計算機并不總是如此無處不在! 直到20世紀70年代末,大型機才是常態,開發人員在同時考慮問題時使用的常見上下文是程序的過程。
>現在許多開發人員正在使用分布式系統,它正在向另一種方向轉移! 我們現在開始考慮虛擬機管理程序,容器和虛擬機作為我們的并發環境。
我們可以合理地期望一臺機器上的一個進程不受另一臺機器上的進程的影響(假設它們不屬于同一個分布式系統),但是我們可以期望同一臺機器上的兩個進程不會影響另一臺機器上的邏輯嗎?進程A可能會覆蓋進程B正在讀取的某些文件,或者在不安全的操作系統中,進程A甚至可能會破壞正在讀取的進程B。
盡管如此,在流程層面上,事情仍然相對容易考慮。 如果我們回到我們的計算器示例,那么期望在同一臺計算機上運行兩個計算器進程的兩個用戶合理地期望他們的操作在邏輯上彼此隔離是合理的。 幸運的是,過程邊界和操作系統幫助我們以合理的方式思考這些問題。 但是我們可以看到,開發人員開始擔心并發問題,并且這個問題只會變得更糟。
如果我們再向下移動到操作系統線程邊界,“為什么是并發編程如此困難”一節中列舉的所有問題才真正出現:競爭條件,死鎖,活鎖和饑餓。 如果我們有一臺機器上的所有用戶都可以查看的計算器進程,那么并發邏輯就會變得更加困難。 我們不得不開始擔心同步對內存的訪問的影響并為正確的用戶檢索正確結果煩惱。
當我們開始向下移動抽象層時,對事物的建模變得更加難以推理,抽象對我們來說變得越來越重要。 換句話說,獲得并發權越困難,訪問容易編寫的并發原語就越重要。 不幸的是,我們行業中的大多數并發邏輯都是以最高抽象層次之一編寫的:系統線程。
在Go出現前,這是大多數流行編程語言的抽象鏈的最終解決方案。如果你想編寫并發代碼,你可以用線程來建模你的程序并同步它們之間的內存訪問。 如果你有很多事情需要同時建模,并且你的機器不能處理那么多的線程,你會創建一個線程池并將你的操作復用到線程池中。
Go在該鏈中添加了另一個鏈接:goroutine。 此外,Go借鑒了著名計算機科學家托尼霍爾的著作中的幾個概念,并引入了我們使用的新原語,即通道(channel)。
繼續我們的推理,會發現在系統線程之下引入另一個抽象層次會帶來更多困難,但有趣的是,事實并非如此。 它實際上使事情變得更容易 這是因為我們沒有在操作系統線程的頂部添加另一個抽象層,我們(在用Go的時候)已經取代了他們。
當然,線程仍然存在,但是我們發現很少需要再從操作系統線程的角度考慮我們的問題空間。 相反,我們在goroutines和channel中建模,偶爾共享內存。 這會產生一些有趣的屬性,我們會逐步探討。但首先,讓我們了解下Go哲學的基石:Tony Hoare的開創性論文“序列化交互”。
* * * * *
學識淺薄,錯誤在所難免。我是長風,歡迎來Golang中國的群(211938256)就本書提出修改意見。
- 前序
- 誰適合讀這本書
- 章節導讀
- 在線資源
- 第一章 并發編程介紹
- 摩爾定律,可伸縮網絡和我們所處的困境
- 為什么并發編程如此困難
- 數據競爭
- 原子性
- 內存訪問同步
- 死鎖,活鎖和鎖的饑餓問題
- 死鎖
- 活鎖
- 饑餓
- 并發安全性
- 優雅的面對復雜性
- 第二章 代碼建模:序列化交互處理
- 并發與并行
- 什么是CSP
- CSP在Go中的衍生物
- Go的并發哲學
- 第三章 Go的并發構建模塊
- Goroutines
- sync包
- WaitGroup
- Mutex和RWMutex
- Cond
- Once
- Pool
- Channels
- select語句
- GOMAXPROCS
- 結論
- 第四章 Go的并發編程范式
- 訪問范圍約束
- fo-select循環
- 防止Goroutine泄漏
- or-channel
- 錯誤處理
- 管道
- 構建管道的最佳實踐
- 便利的生成器
- 扇入扇出
- or-done-channel
- tee-channel
- bridge-channel
- 隊列
- context包
- 小結
- 第五章 可伸縮并發設計
- 錯誤傳遞
- 超時和取消
- 心跳
- 請求并發復制處理
- 速率限制
- Goroutines異常行為修復
- 本章小結
- 第六章 Goroutines和Go運行時
- 任務調度