GCTT 出品 | Go vs Python:深入並發

2019-08-27     Go語言中文網

介紹

在過去的幾個月里,我在幾個項目上使用過 Go,儘管我還算不上專家,但是還是有幾件事我要感謝 Go:首先,它有一個清晰而簡單的語法,我不止一次注意到 Github 開發人員的風格非常接近於舊 C 程序中使用的風格,從理論上講,Go 似乎吸收了世界上所有語言最好的特性:它有著高級語言的力量,明確的規則使得更簡單,即使這些特性有時有一點點的約束力--就是可以給代碼強加一個堅實的邏輯。這是命令式的簡單,由大小以位為單位的原始類型組成。但是沒有像把字符串當成字符數組那樣操作的乏味。然而,我認為這兩個非常有用和有趣的功能是 goroutine 和 channels。

前言

為了理解 Go 為什麼能更好地處理並發性,首先需要知道什麼是並發性 1[1]。並發性是獨立執行計算的組成部分:是一種更好地編寫與現實世界進行良好交互的乾淨的代碼的方法。通常,即使並發不等同於並行,人們也會將並發的概念與並行的概念混淆:是,儘管它能夠實現並行性。所以,如果你只有一個處理器,你的程序仍然可以並發,但不能並行。另一方面,良好的並發程序可以在多處理器上並行運行 2[2]。這一特性是非常重要的。讓我們來談談 Go 如何讓程序利用在多處理器 / 多線程環境中運行的優勢。或者說,Go 提供了什麼工具來編寫並發程序,因為它不是關於線程或核心的:它是關於 routine 的。

Goroutine

假設我們調用一個函數 f(s):這樣的寫法就是通常的調用方式,同步運行。如果要在 goroutine 中調用這個函數,使用 go f(s) 即可。這個新 goroutine 將和調用它的 goroutine 並發執行。但是... 什麼是 goroutine 呢?這是一個獨立執行的函數,由 go 語句啟動。它有自己的調用堆棧,這個堆棧可以根據需要增長和縮減,而且非常節省空間。擁有數千甚至數十萬個 goroutine 是實際存在的,但它不是線程。事實上,在一個有數千個 goroutine 的程序中可能只有一個線程。相反,goroutines 會根據需要動態復用到線程上,以保持所有的 goroutine 運行。如果你把它當成一種便宜的線程,也不會差太多。

更多細節 3

正如我所說的,coroutine 背後的想法是復用獨立執行的函數--coroutines--在一組線程上。當一個 coroutine 阻塞的時候,比如通過調用一個阻塞的系統調用, run-time 會自動地將同一個作業系統線程上的其他 coroutines 移動到一個不同的,可運行的線程上,這樣它們就不會被阻塞。這些 coroutines 被稱為 goroutines,非常便宜。它們的堆棧內存很少,只有幾千位元組。此外,為了使堆棧變小,Go 的 run-time 使用可調整大小的有界堆棧。新建的 goroutine 有幾千位元組,這個大小几乎總是足夠的。當空間不夠時,run-time 會自動增長(縮小)用於存儲堆棧的內存,從而允許許多 goroutines 生存在適量的內存中。每個函數調用的 CPU 開銷平均需要大約三個廉價的指令,所以在相同的地址空間中創建數十萬個 goroutine 是很實際的。如果 goroutines 只是線程,那麼系統資源將會用得更少。

好吧,真的很酷,但... 為什麼?為什麼我們要編寫並發程序?要更快地完成我們的工作(即使編寫正確的並發程序可能花費的時間比在並行環境中運行任務的時間長 XD)典型的線程情況包括分配一些共享內存並將其位置存儲在 p 中的主線程。主線程啟動 n 個工作線程,將指針 p 傳遞給他們,工作線程可以使用 p 來處理 p 指向的數據。但是如果線程開始更新相同的內存地址呢?我是說,這是計算機科學中最難的一個。好吧,讓我們從簡考慮:從作業系統的角度來看,一些原子系統調用讓你鎖定對共享內存區域的訪問(我是指信號量,消息隊列,鎖等)。從語言角度來看,通常有一組原語,調用所需的系統調用,並讓你將訪問權限同步到共享內存區域(我是指像多處理,多線程,池等的包)。下面,我們來談談 Go 的一個工具,它可以幫助您處理 goroutine 之間的並發通信:channels。

Channels

Channels 是一個輸入管道,你可以通過通道操作符 <- 發送和接收值。這就是全部:D. 你只需要知道當一個 main 函數執行 <-c 時,它將等待一個值被發送。同樣,當 goroutined 函數執行 c<-value 值時,它等待接收器準備就緒。發送者和接收者都必須準備好,來在通信中發揮作用。否則,我們要等到它們準備好:你不必處理信號量,鎖等等:channels 可以同時實現通信和同步。記住和理解這一點非常重要,也是 Go 和我所知道的其他語言之間最大的區別之一。

更多細節 4

正如官方文檔所述,channel 提供了一種機制,用於通過發送和接收指定元素類型的值來並發執行函數來進行通信。這很簡單。我還沒有說的是,一個 channel 作為一種類型,不同於它承載的信息類型:

ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType

可選的 <- 運算符指定通道方向,發送或接收. 如果沒有方向,通道是雙向的。channel 可能僅限於發送或僅通過轉換或分配接收。

chan T // can be used to send and receive values of type T
chan<- float64 // can only be used to send float64s
<-chan int // can only be used to receive ints

為了幫助您解決某些特定的同步問題,您還可以使用 make(make(chan int,100)) 函數創建一個 buffered channel. 容量, 也就是按元素數量,設置 channel 中緩衝區的大小。如果容量為零或不存在,則只有在發送方和接收方都準備好的情況下,channel 才能無緩存,並且通信成功。否則,如果緩衝區未滿(發送)或不空閒(接收),則 channel 被緩衝並且通信成功而不阻塞。 一個零 channel 永遠不會準備好通訊:我發現通過使用緩衝 channel,可以隱式地設置在運行時要使用的最大程序數量,這對於我的基準測試是非常有用的。

總結

總而言之,你可以在 goroutine 中調用一個函數,甚至是匿名函數, 然後把結果放在一個 channel 中,默認情況下,發送和接收阻塞,直到另一端準備好。所有這些特性都允許 goroutine 在沒有顯式鎖定或條件變量的情況下進行同步。好吧,但是... 他們表現地怎麼樣呢?

Go vs Python

好吧,我是一個 Python 愛好者-我想,因為它在標題中,我不記得. md 各自的原始碼在哪裡. 所以我決定做一個比較,看看這些神奇的 Go 巧妙的語句如何真正執行。為此,我編寫了一個簡單的 go-py 程序(這裡[5] 是代碼),它完成了對隨機整數列表的合併排序,可以在單核環境或多核環境中運行。或者,在單個_例程或多個_例程_環境中:這是因為,正如我所說的,go-routine 是一個在 Python 中不可用的概念,比線程更深入。請記住,不止一個 go-routine 可以屬於一個單獨的線程。相反,從 Python 的角度來看,你只能使用進程,線程以及信號量,鎖定,鎖等等,但不可能重現完全相同的計算。我的意思是,這是正常的,他們是不同的語言,但他們最後都調用一組系統調用。無論如何,我認為當你運行這種並發性實驗時,你可以做的是儘可能地重現一個在邏輯上的等價性的計算。我們從 Go 版本開始。

Go 合併排序

Go 和 Python 版本的程序都提供了兩個功能:

  • 單 routine;
  • 多個前綴數的 routine;

簡單的 Go 版本

好吧,我不會講太多關於單 routine 的方法:這很簡單。下面你可以看到我能夠考慮的最優化版本的代碼(就 io 操作而言), Github[6] 上的評論版本:

我不認為這需要解釋:如果您有任何問題,請不要猶豫在評論中寫下意見!我會儘快回答。

並發的 Go 版本

我們來談談並發版本。我們可以拆分數組,並從主例程調用子例程,但是我們如何控制並發執行 go-routine 或工作數的最大數量?那麼,限制 Go 中的並發的一種方法 5[7] 是使用緩衝通道(信號量)。正如我所說的,當你創建一個具有固定維度的通道或緩衝,如果緩衝區未滿(發送)或不為空(接收),通信成功而不會阻塞,所以你根據你想擁有的並發單元的數量,實現一個信號量來輕鬆地阻止執行。真的很酷,但是... 有一個問題:一個 channel 是一個 channel,即使有緩衝,頻道上的基本發送和接收也被阻止。幸運的是,Go 非常棒,讓你創建明確的非阻塞通道, 使用 select 語句 6[8] :因此,您可以使用 select with default 子句來實現無阻塞的發送,接收,甚至是非阻塞的多路選擇。還有一些其他的聲明來解釋,在我的前綴最大數量的並發 goroutine 版本的合併排序:

正如你所看到的,在我的默認選擇操作中,我編寫了一個調用單 routined 版本的合併排序。但是,代碼中還有一個有趣的工具:它是由 sync 包提供的 WaitGroup 對象。從官方文檔 7[9] 來看 ,WaitGroup 等待一系列 goroutines 完成。main goroutine 調用 Add 來設置要等待的 goroutines 的數量。然後,每個 goroutine 程序運行並完成後調用 Done。同時,Wait 可以用來阻塞,直到所有的 goroutines 都完成了。

Python 合併排序

好吧,在這裡,如果你到了這裡,我會誠實的說:我不是一個並發專家,實際上我真的討厭並發,但是寫這篇文章和測試 Go channel 讓我學到了很多關於這個主題的知識:在 Python 中儘可能複製一個在邏輯上大部分相同的計算真的很難。

簡單的 Py 版本

並發 Py 版本

我不得不為這個並發版本想很多:首先,我想使用一個線程 / 進程數組,並啟動 / 加入他們,但是,後來我意識到這與我的 Go 版本不太一樣。首先,因為對多於一個線程 / 進程的調用只能在原始數據的一個分區上完成一次, 最終以並行合併的方式合併:這不完全是我的 Go 版本的行為,遞歸調用一個並發例程,直到信號量接受新的並發例程,最後調用排序方法的單例程實例。所以我想 「我簡直不可能在 Python 中使用簡單的一次性分裂方法來實現我的合併排序的多例程(線程或進程),因為它不是計算上等同的」。出於這個原因,我嘗試的第一件事是使用 Python 中的信號量原語重新表示 Channel 和 WaitGroup 的完全相同的行為。經過幾天的工作,我得到了它。讓我們看看代碼:

好吧,讓我們從 manager 開始。在主體中初始化的 Manager 對象提供了一個結構來放置調用的響應 - 或多或少類似於 Queue。BoundedSemaphore 扮演著我之前談到的有界 channel 信號量的角色。信號量是一個比簡單的鎖更高級的鎖機制:它有一個內部的計數器而不是一個鎖定標誌,並且只有當超過給定數量的線程試圖持有信號才會阻塞它。根據信號量的初始化方式,這允許多個線程同時訪問相同的代碼段:幸運的是,如果你失敗了,你可以嘗試獲得鎖定並繼續執行--這起到了前面提到的在 Go 版本中使用的 select 技巧, 通過使用 blocking = False 作為 (bufferedChannel.acquire(blocking = False)) 的參數。有了 join,我模擬了 WaitGroup 的行為,因為我認為這是在繼續最後的合併步驟之前同步這兩個線程並等待它們結束的標準方式。這裡有任何問題麼?

你想知道"它的表現怎麼樣"好吧,它遜爆了。我的意思是:非常遜。所以我試圖尋找更有效率的東西... 我找到了這個. 類似於我想到的第一個解決方案,但使用 Pool 對象。

而且這個表現更好。問題是使用線程或進程更好?那麼,看看我的比較圖!

好吧,因為 Python 版本不太好,這是一個只有 Go 系列的圖表

結論

Python 糟透了。 Go 完勝。對不起,Python:我愛你。完整的代碼可以在這裡找到:go-py-benchmark[10]

謝謝大家的閱讀!

1  這裡可以在線獲得很多關於 Go talk 的 幻燈片[11]! 2. Rob Pike 的課程[12]:並發不是並行的。 3.  直接來源官方 FAQ[13] 頁面。 4.  更多信息在 這裡[14]。 5.  來源 在這[15] 6.  看看 這裡[16] 7.  這裡更多關於 WaitGroup[17] 的信息


via: https://made2591.github.io/posts/go-py-benchmark

作者:Matteo Madeddu[18] 譯者:Titanssword[19] 校對:polaris1119[20]

本文由 GCTT[21] 原創編譯,Go 中文網[22] 榮譽推出

參考資料

  • [5]
  • 這裡: https://github.com/made2591/go-py-benchmark
  • [6]
  • Github: https://github.com/made2591/go-py-benchmark/blob/master/main.go
  • [10]
  • go-py-benchmark: https://made2591.github.io/posts/go-py-benchmark
  • [11]
  • 幻燈片: https://talks.Go.org/2012/concurrency.slide
  • [12]
  • Rob Pike 的課程: https://vimeo.com/49718712
  • [13]
  • FAQ: https://Go.org/doc/faq
  • [14]
  • 這裡: https://Go.org/ref/spec#Channel_types
  • [15]
  • 在這: https://medium.com/@_orcaman/when-too-much-concurrency-slows-you-down-Go-9c144ca305a
  • [16]
  • 這裡: https://gobyexample.com/non-blocking-channel-operations
  • [17]
  • WaitGroup: https://Go.org/pkg/sync/#WaitGroup
  • [18]
  • Matteo Madeddu: https://made2591.github.io/about/
  • [19]
  • Titanssword: https://github.com/Titanssword
  • [20]
  • polaris1119: https://github.com/polaris1119
  • [21]
  • GCTT: https://github.com/studyGo/GCTT
  • [22]
  • Go 中文網: https://studygolang.com/

文章來源: https://twgreatdaily.com/zh-cn/5uMC12wBJleJMoPMrPAi.html