Golang 并發(fā)之一 ( go并發(fā)模型)

如果必須選擇 Go 的一項(xiàng)偉大功能,那么它必須是內(nèi)置的并發(fā)模型。它不僅支持并發(fā),而且使它變得更好。 Go Concurrency Model (goroutines) 之于并發(fā),就像 Docker 之于虛擬化。

什么是并發(fā)(Concurrency)?

在計(jì)算機(jī)編程中,并發(fā)(Concurrency)是計(jì)算機(jī)同時(shí)處理多個(gè)事物的能力。例如,如果您在瀏覽器中上網(wǎng),可能會(huì)同時(shí)發(fā)生很多事情。在特定情況下,您可能正在下載一些文件,同時(shí)在您滾動(dòng)的頁面上聽一些音樂。因此瀏覽器需要同時(shí)處理很多事情。如果瀏覽器無法立即處理它們,您需要等待所有下載完成,然后您才能再次開始瀏覽互聯(lián)網(wǎng)。那會(huì)令人沮喪。

一般來說 PC 可能只有一個(gè) CPU 內(nèi)核來完成所有的處理和計(jì)算。一個(gè) CPU 內(nèi)核同一時(shí)間只能處理一件事。當(dāng)我們談?wù)摬l(fā)(concurrency)時(shí),我們一次只做一件事,但可以將 CPU 時(shí)間分配給需要處理的事情。因此,我們會(huì)感覺同時(shí)發(fā)生了多種事情,但事實(shí)上一次只發(fā)生了一件事情。

我們通過圖表來了解上述討論的案例: CPU 通過 Web 瀏覽器如何"同時(shí)"處理多個(gè)事情。

cpu 并發(fā)模型

所以從上圖可以看出,單核處理器幾乎是根據(jù)每個(gè)任務(wù)的優(yōu)先級來劃分工作負(fù)載的,例如,在頁面滾動(dòng)時(shí),聽音樂的優(yōu)先級可能較低,因此有時(shí)您的音樂會(huì)因低優(yōu)先級而停止互聯(lián)網(wǎng)速度,但您仍然可以滾動(dòng)頁面。

什么是并行(parallelism)?

但是問題來了,如果 CPU 有多個(gè)內(nèi)核呢?如果一個(gè)處理器有多個(gè)處理器,則稱為多核處理器。多核處理器能夠同時(shí)處理多項(xiàng)事情。 在之前的網(wǎng)頁瀏覽示例中,我們的單核處理器必須在不同的事物之間分配 CPU 時(shí)間。使用多核處理器,我們可以在不同的內(nèi)核中同時(shí)運(yùn)行不同的東西。讓我們使用下圖來評估。

cpu 并行模型

并行運(yùn)行不同事物的概念稱為并行(parallelism)性。當(dāng)我們的 CPU 有多個(gè)內(nèi)核時(shí),我們可以使用不同的 CPU 內(nèi)核同時(shí)做多事情。因此,我們可以說我們可以很快完成一項(xiàng)工作(包括很多東西),但事實(shí)并非如此。我們后面在討論這一點(diǎn)。

并發(fā) (concurrency) vs 并行 (parallelism)

Go 建議只在一個(gè)內(nèi)核上使用 goroutines,但我們可以修改 Go 程序以在不同的處理器內(nèi)核上運(yùn)行 goroutines。現(xiàn)在,將 goroutines 視為 Go 函數(shù),因?yàn)樗鼈兙褪牵€有更多。

并發(fā)性和并行性之間有幾個(gè)區(qū)別。并發(fā)一個(gè)人在同一時(shí)間處理多個(gè)事情,多個(gè)事情之間回切換;而并行是多人同時(shí)處理多個(gè)事情,每個(gè)人處理其中的一件。但并行并不總是比并發(fā)更有利,我們在下一片文章來討論這個(gè)問題。

此時(shí),您的腦海中可能會(huì)有很多問題,您可能已經(jīng)有了并發(fā)的想法,但您可能想知道 Go 如何實(shí)現(xiàn)它以及如何使用它。要了解 Go 的并發(fā)架構(gòu)以及如何在代碼中使用它,以及何時(shí)在應(yīng)用程序中使用它,我們需要了解什么是計(jì)算機(jī)進(jìn)程。

計(jì)算機(jī)進(jìn)程(process)是什么?

當(dāng)您使用 C、java 或 Go 等語言編寫計(jì)算機(jī)程序時(shí),它只是一個(gè)文本文件。但是由于計(jì)算機(jī)只能理解由 0 和 1 組成的二進(jìn)制指令,因此需要將該代碼編譯為機(jī)器語言。這就是編譯器的用武之地。在 python 和 javascript 等腳本語言中,解釋器做同樣的事情。

當(dāng)一個(gè)編譯好的程序被送到操作系統(tǒng)(OS) 處理時(shí),操作系統(tǒng)(os) 會(huì)分配不同的東西,比如內(nèi)存地址空間(進(jìn)程的堆和棧所在的位置)、程序計(jì)數(shù)器、PID(進(jìn)程 ID)和其他非常重要的東西。一個(gè)進(jìn)程至少有一個(gè)線程稱為主線程,而主線程可以創(chuàng)建多個(gè)其他線程。當(dāng)主線程執(zhí)行完畢后,進(jìn)程退出。

所以我們理解進(jìn)程是一個(gè)容器,它已經(jīng)編譯了代碼、內(nèi)存、不同的操作系統(tǒng)資源和其他可以提供給線程的東西。簡而言之,進(jìn)程就是內(nèi)存中的一個(gè)程序。但是什么是線程,它們的工作是什么?

什么是線程(thread)?

線程是進(jìn)程內(nèi)的輕量級進(jìn)程。線程是一段代碼的實(shí)際執(zhí)行者。線程可以訪問進(jìn)程提供的內(nèi)存、操作系統(tǒng)資源和其他東西。

在執(zhí)行代碼時(shí),線程在內(nèi)存區(qū)域內(nèi)存儲變量(數(shù)據(jù))稱為堆棧,其中臨時(shí)空間變量保存臨時(shí)空間。堆棧在運(yùn)行時(shí)創(chuàng)建,通常具有固定大小,最好為 1-2 MB。而一個(gè)線程的堆棧只能由該線程使用,不會(huì)與其他線程共享。堆是進(jìn)程的一個(gè)屬性,可供任何線程使用。堆是一個(gè)共享內(nèi)存空間,來自一個(gè)線程的數(shù)據(jù)也可以被其他線程訪問。

現(xiàn)在我們大致了解了進(jìn)程和線程。但是它們有什么用呢?

當(dāng)啟動(dòng) Web 瀏覽器時(shí),必須有一些代碼指示操作系統(tǒng)執(zhí)行某些操作。這意味著我們正在創(chuàng)建一個(gè)進(jìn)程。該進(jìn)程可能會(huì)要求操作系統(tǒng)為新選項(xiàng)卡創(chuàng)建另一個(gè)進(jìn)程。當(dāng)瀏覽器選項(xiàng)卡打開并且同時(shí)您正在做日常工作,該選項(xiàng)卡進(jìn)程將開始為不同的活動(dòng)(如頁面滾動(dòng)、下載、聽音樂等)創(chuàng)建不同的線程,正如我們在之前的圖表中看到的那樣。

下面是 macOS 平臺上 Chrome 瀏覽器應(yīng)用程序的屏幕截圖

process on mac

上面的屏幕截圖顯示 Google Chrome 瀏覽器對打開的標(biāo)簽頁和內(nèi)部服務(wù)使用不同的進(jìn)程。由于每個(gè)進(jìn)程至少有一個(gè)線程,我們可以看到一個(gè)谷歌瀏覽器進(jìn)程,在這種情況下,有超過 3 個(gè)線程。

在之前的話題中,我們談到了處理多件事或做多件事。這里的事物是由線程執(zhí)行的活動(dòng)。因此,當(dāng)在并發(fā)或并行模式下發(fā)生多件事情時(shí),會(huì)有多個(gè)線程串聯(lián)或并行運(yùn)行,也就是多線程。

在多線程中,在一個(gè)進(jìn)程中產(chǎn)生多個(gè)線程,內(nèi)存泄漏的線程會(huì)耗盡其他線程的資源并使進(jìn)程無響應(yīng)。在使用瀏覽器或任何其他程序時(shí),您可能已經(jīng)多次看到這種情況。您可能已經(jīng)使用活動(dòng)監(jiān)視器或任務(wù)管理器來查看無響應(yīng)的進(jìn)程并殺死它。

線程調(diào)度

當(dāng)多個(gè)線程串行或并行運(yùn)行時(shí),由于多個(gè)線程可能共享一些數(shù)據(jù),因此線程需要協(xié)同工作,以便一次只有一個(gè)線程可以訪問特定數(shù)據(jù)。以某種順序執(zhí)行多個(gè)線程稱為調(diào)度。操作系統(tǒng)線程由內(nèi)核調(diào)度,一些線程由編程語言的運(yùn)行時(shí)環(huán)境管理,如 JRE。當(dāng)多個(gè)線程試圖同時(shí)訪問相同的數(shù)據(jù)導(dǎo)致數(shù)據(jù)被更改或?qū)е乱馔饨Y(jié)果時(shí),就會(huì)發(fā)生競爭條件。

在設(shè)計(jì)并發(fā) Go 程序時(shí),我們需要注意競爭條件,我們將在下一片文章中討論。

single thread vs multi thread

Go 中的并發(fā)(concuenry)

最后,我們將討論 Go 如何實(shí)現(xiàn)并發(fā)。像java這樣的傳統(tǒng)語言有一個(gè)線程類,可以用來在當(dāng)前進(jìn)程中創(chuàng)建多個(gè)線程。由于 Go 沒有傳統(tǒng)的 OOP 語法,它提供了 go 關(guān)鍵字來創(chuàng)建 goroutine。當(dāng) go 關(guān)鍵字放在函數(shù)調(diào)用之前,它就變成了 goroutines。

我們將在下一片文章中討論 goroutines,但簡而言之,goroutines 的行為類似于線程,但在技術(shù)上;它是對線程的抽象。

當(dāng)我們運(yùn)行 Go 程序時(shí),Go runtime 將在一個(gè)核心上創(chuàng)建幾個(gè)線程,所有 goroutine 都在該核心上復(fù)用(產(chǎn)生)。在任何時(shí)候,一個(gè)線程將執(zhí)行一個(gè) goroutine,如果該 goroutine 被阻塞,那么它將被替換為另一個(gè)將在該線程上執(zhí)行的 goroutine。這就像線程調(diào)度,但由 Go runtime 處理,而且速度要快得多。

在大多數(shù)情況下,建議在一個(gè)內(nèi)核上運(yùn)行所有 goroutines,但是如果您需要在系統(tǒng)的可用 CPU 內(nèi)核之間劃分 goroutines,您可以使用 GOMAXPROCS 環(huán)境變量或使用函數(shù) runtime.GOMAXPROCS(n) 調(diào)用運(yùn)行時(shí)其中 n 是要使用的內(nèi)核數(shù)。但是有時(shí)您可能會(huì)覺得設(shè)置 GOMAXPROCS > 1 會(huì)使您的程序變慢。這確實(shí)取決于程序的性質(zhì),但您可以在互聯(lián)網(wǎng)上找到問題的解決方案或解釋。實(shí)際上,當(dāng)程序使用多核、操作系統(tǒng)線程和進(jìn)程時(shí),在通道上通信比在計(jì)算上花費(fèi)更多時(shí)間的程序會(huì)遇到性能下降。

Go 有一個(gè) M:N 調(diào)度器,它也可以使用多個(gè)處理器。在任何時(shí)候,都需要在 N 個(gè)操作系統(tǒng)線程上調(diào)度 M 個(gè) goroutine,這些線程最多在 GOMAXPROCS 個(gè)處理器上運(yùn)行。在任何時(shí)候,每個(gè)內(nèi)核最多只能運(yùn)行一個(gè)線程。但是調(diào)度程序可以根據(jù)需要?jiǎng)?chuàng)建更多線程,但這很少發(fā)生。如果你的程序沒有啟動(dòng)任何額外的 goroutines,那么無論你允許它使用多少個(gè)內(nèi)核,它自然只會(huì)在一個(gè)線程中運(yùn)行。

線程(threads) vs goroutines

正如我們之前看到的,線程和 goroutines 之間存在明顯的區(qū)別,但下面的區(qū)別將闡明為什么線程比 goroutines 更昂貴,以及為什么 goroutines 是實(shí)現(xiàn)應(yīng)用程序中最高級別并發(fā)的關(guān)鍵解決方案。

thread goroutines
操作系統(tǒng)線程由內(nèi)核管理并具有硬件依賴性。 goroutines 由 go runtime管理,沒有硬件依賴。
OS 線程通常具有 1-2MB 的固定堆棧大小 在較新版本的 go 中,goroutines 通常具有 8KB(自 Go 1.4 以來為 2KB)的堆棧大小
堆棧大小在編譯時(shí)確定,不能增長 go 的堆棧大小在運(yùn)行時(shí)進(jìn)行管理,可以通過分配和釋放堆存儲增加到 1GB
線程之間沒有簡單的通信媒介。線程間通信之間存在巨大的延遲。 goroutine 使用通道以低延遲與其他 goroutine 通信
線程具有標(biāo)識。有 TID 標(biāo)識進(jìn)程中的每個(gè)線程。 goroutine 沒有任何身份。 go 實(shí)現(xiàn)了這一點(diǎn),因?yàn)?go 沒有 TLS(線程本地存儲Thread Local Storage)。
線程具有顯著的設(shè)置和拆卸成本,因?yàn)榫€程必須從操作系統(tǒng)請求大量資源并在完成后返回 goroutine 由 go runtime 創(chuàng)建和銷毀。與線程相比,這些操作非常便宜,因?yàn)?go runtime 已經(jīng)為 goroutine 維護(hù)了線程池。在這種情況下,操作系統(tǒng)不知道 goroutines。
線程是預(yù)先調(diào)度的。由于調(diào)度程序需要保存/恢復(fù)超過 50 個(gè)寄存器和狀態(tài),因此線程之間的切換成本很高。當(dāng)線程之間快速切換時(shí),這可能非常重要。 goroutine 是協(xié)同調(diào)度的。當(dāng)發(fā)生 goroutine 切換時(shí),只需要保存或恢復(fù) 3 個(gè)寄存器。

以上是一些重要的區(qū)別,但如果你深入研究,你會(huì)發(fā)現(xiàn) Go 并發(fā)模型的驚人世界。為了突出 Go 并發(fā)強(qiáng)度的一些優(yōu)勢,假設(shè)您有一個(gè) Web 服務(wù)器,每分鐘處理 1000 個(gè)請求。如果您必須同時(shí)運(yùn)行每個(gè)請求,則意味著您需要?jiǎng)?chuàng)建 1000 個(gè)線程或?qū)⑺鼈儎澐值讲煌倪M(jìn)程中。這就是 Apache 服務(wù)器管理傳入請求的方式。如果 OS 線程每個(gè)線程消耗 1MB 堆棧大小,則意味著您將耗盡 1GB RAM 用于該流量。 Apache 提供了 ThreadStackSize 指令來管理每個(gè)線程的堆棧大小,但您仍然不知道是否因此而遇到問題。

而在 goroutine 的情況下,由于堆棧大小可以動(dòng)態(tài)增長,您可以毫無問題地生成 1000 個(gè) goroutine。由于 goroutine 以 8KB(自 Go 1.4 以來為 2KB)的堆棧空間開始,它們中的大多數(shù)通常不會(huì)增長得比這更大。但是,如果存在需要更多內(nèi)存的遞歸操作,Go 可以將堆棧大小增加到 1GB,我認(rèn)為這幾乎不會(huì)發(fā)生,除了 for {}, 這顯然是一個(gè)錯(cuò)誤。

此外,與我們之前看到的線程相比,goroutines 之間的快速切換是可能的并且更高效。由于一個(gè) goroutine 一次在一個(gè)線程上運(yùn)行并且 goroutine 是協(xié)作調(diào)度的,因此在當(dāng)前 goroutine 被阻塞之前不會(huì)調(diào)度另一個(gè) goroutine。如果該線程塊中的任何 Goroutine 說等待用戶輸入,那么另一個(gè) goroutine 將被安排在它的位置。

goroutine 可以在遇到以下條件之一時(shí)阻塞

  • network input
  • sleeping
  • channel operation
  • blocking on primitives in the sync package

如果 goroutine 沒有在這些條件之一上阻塞,它可以使多路復(fù)用的線程餓死,殺死進(jìn)程中的其他 goroutine。雖然有一些補(bǔ)救措施,但如果確實(shí)如此,那么它就被認(rèn)為是糟糕的編程。

Channels 在goroutine 間共享數(shù)據(jù)時(shí),將發(fā)揮重要作用,下一章我們將討論這個(gè)問題。這將防止競爭條件和對共享數(shù)據(jù)的不當(dāng)訪問,而不應(yīng)在多線程的情況下訪問共享內(nèi)存。

更多參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,899評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內(nèi)容

  • 1. C/C++ 與 Go語言的“價(jià)值觀”對照 C的價(jià)值觀摘錄 相信程序員:提供指針和指針運(yùn)算,讓C程序員天馬行空...
    ywhu閱讀 6,917評論 0 13
  • 正文開始之前先拋出一個(gè)思考:讓一個(gè)靜態(tài)網(wǎng)站滿足海量用戶訪問本質(zhì)上是一個(gè)并行問題還是并發(fā)問題? 并發(fā)的世界 并發(fā)這個(gè)...
    謝培陽閱讀 1,981評論 3 16
  • 控制并發(fā)有三種種經(jīng)典的方式,一種是通過channel通知實(shí)現(xiàn)并發(fā)控制 一種是WaitGroup,另外一種就是Con...
    wiseAaron閱讀 10,681評論 4 34
  • Go 并發(fā)編程 選擇 Go 編程的原因可能是看中它簡單且強(qiáng)大,那么你其實(shí)可以選擇C語言;除此之外,我看中 Go 的...
    PRE_ZHY閱讀 900評論 1 6
  • 我是黑夜里大雨紛飛的人啊 1 “又到一年六月,有人笑有人哭,有人歡樂有人憂愁,有人驚喜有人失落,有的覺得收獲滿滿有...
    陌忘宇閱讀 8,577評論 28 53