Golang 併發編程

前言

簡而言之,所謂併發編程是指在一臺處理器上「同時」處理多個任務。linux

隨着硬件的發展,併發程序變得愈來愈重要。Web服務器會一次處理成千上萬的請求。平板電腦和手機app在渲染用戶畫面同時還會後臺執行各類計算任務和網絡請求。即便是傳統的批處理問題--讀取數據,計算,寫輸出--如今也會用併發來隱藏掉I/O的操做延遲以充分利用現代計算機設備的多個核心。計算機的性能每一年都在以非線性的速度增加。git

宏觀的併發是指在一段時間內,有多個程序在同時運行。編程

併發在微觀上,是指在同一時刻只能有一條指令執行,但多個程序指令被快速的輪換執行,使得在宏觀上具備多個進程同時執行的效果,但在微觀上並非同時執行的,只是把時間分紅若干段,使多個程序快速交替的執行。windows

image

並行和併發

並行: 並行(parallel):指在同一時刻,有多條指令在多個 CPU 處理器上同時執行。安全

image

併發(concurrency): 指在同一時刻只能有一條指令執行,但多個進程指令被快速的輪換執行。使得在宏觀上具備多個進程同時執行的效果,但在微觀上並非同時執行的,只是把時間分紅若干段,經過 cpu 時間片輪轉使多個進程快速交替的執行。bash

image

  • 宏觀:用戶體驗上,程序在並行執行。
  • 微觀:多個計劃任務,順序執行,在飛快的切換,輪換使用 cpu 時間輪片。

大師曾以咖啡機的例子來解釋並行和併發的區別:服務器

image

  • 並行是兩個隊列同時使用兩臺咖啡機 (真正 的多任務)
  • 併發是兩個隊列交替使用一臺咖啡機 ( 的多任務)

常見併發編程技術

進程併發

程序和進程

程序,是指編譯好的二進制文件,只佔用磁盤空間,不佔用系統資源(cpu、內存、打開的文件、設備、鎖 ...)網絡

進程,是一個抽象的概念,與操做系統原理聯繫緊密。進程是活躍的程序,佔用系統資源。在內存中執行。(程序運行起來,產生一個進程)數據結構

程序 → 劇本(紙) 進程 → 戲(舞臺、演員、燈光、道具 ...)多線程

同一個劇本能夠在多個舞臺同時上演。一樣,同一個程序也能夠加載爲不一樣的進程(彼此之間互不影響)

如:同時開兩個終端。各自都有一個 bash 但彼此 ID 不一樣。

在 windows 系統下,經過查看「任務管理器」,能夠查看相應的進程。運行起來的程序就是一個進程。以下圖所示:

image

進程狀態

進程基本的狀態有5種。分別爲初始態就緒態運行態掛起(阻塞)態終止(中止)態。其中初始態爲進程準備階段,常與就緒態結合來看。

image

進程併發

在使用進程 實現併發時會出現什麼問題呢?

  • 系統開銷比較大,佔用資源比較多,開啓進程數量比較少。
  • 在 unix/linux 系統下,還會產生孤兒進程殭屍進程

經過前面查看操做系統的進程信息,咱們知道在操做系統中,能夠產生不少的進程。

在 unix/linux 系統中,正常狀況下,子進程是經過父進程 fork 建立的,子進程再建立新的進程。

而且父進程永遠沒法預測子進程到底何時結束。 當一個 進程完成它的工做終止以後,它的父進程須要調用系統調用取得子進程的終止狀態。

孤兒進程:父進程先於子進程結束,則子進程成爲孤兒進程,子進程的父進程成爲 init 進程,稱爲 init 進程領養孤兒進程。

殭屍進程:子進程終止,父進程還沒有回收,子進程殘留資源(PCB)存放於內核中,變成殭屍(Zombie)進程。

Windows 下的進程和 Linux 下的進程是不同的,它比較懶惰,歷來不執行任何東西,只是爲線程提供執行環境。而後由線程負責執行包含在進程的地址空間中的代碼。當建立一個進程的時候,操做系統會自動建立這個進程的第一個線程,成爲主線程。

線程併發

什麼是線程

LWP:light weight process 輕量級的進程,本質還是進程 (Linux下)

進程:獨立地址空間,擁有 PCB

線程:有獨立的 PCB,但沒有獨立的地址空間(共享)

區別:在因而否共享地址空間。獨居(進程);合租(線程)。

image

進程:最小分配資源單位,可當作是隻有一個線程的進程。

線程:最小的執行單位

Windows 系統下,能夠直接忽略進程的概念,只談線程。由於線程是最小的執行單位,是被系統獨立調度和分派的基本單位。而進程只是給線程提供執行環境。

線程同步

同步即協同步調,按預約的前後次序運行。

線程同步,指一個線程發出某一功能調用時,在沒有獲得結果以前,該調用不返回。同時其它線程爲保證數據一致性,不能調用該功能。

舉例1: 銀行存款 5000。櫃檯:取3000;同時提款機:取 3000。剩餘:2000。

舉例2: 內存中 100 字節,線程T1欲填入全1, 線程T2欲填入全0。但若是T1執行了50個字節失去cpu,T2執行,會將T1寫過的內容覆蓋。當T1再次得到cpu繼續 從失去cpu的位置向後寫入1,當執行結束,內存中的100字節,既不是全1,也不是全0。

產生的現象叫作「與時間有關的錯誤」(time related)。爲了不這種數據混亂,線程須要同步。

「同步」的目的,是爲了不數據混亂,解決與時間有關的錯誤。實際上,不只線程間須要同步,進程間、信號間等等都須要同步機制。

所以,全部「多個控制流,共同操做一個共享資源」的狀況,都須要同步。

鎖的應用

互斥量 mutex

Linux 中提供一把互斥鎖 mutex(也稱之爲互斥量)。

每一個線程在對資源操做前都嘗試先加鎖,成功加鎖才能操做,操做結束解鎖。

資源仍是共享的,線程間也仍是競爭的,但經過「鎖」就將資源的訪問變成互斥操做,然後與時間有關的錯誤也不會再產生了。

image

可是應注意:同一時刻,只能有一個線程持有該鎖。

當 A 線程對某個全局變量加鎖訪問,B 在訪問前嘗試加鎖,拿不到鎖,B 阻塞。C 線程不去加鎖,而直接訪問該全局變量,依然可以訪問,但會出現數據混亂。

因此,互斥鎖實質上是操做系統提供的一把「建議鎖」(又稱「協同鎖」),建議程序中有多線程訪問共享資源的時候使用該機制。可是並無強制限定。

所以,即便有了 mutex,若是有線程不按規則來訪問數據,依然會形成數據混亂。

讀寫鎖

與互斥量相似,但讀寫鎖容許更高的並行性。其特性爲:寫獨佔,讀共享

讀寫鎖狀態:

特別強調:讀寫鎖只有一把,但其具有兩種狀態:

  • 讀模式下加鎖狀態 (讀鎖)
  • 寫模式下加鎖狀態 (寫鎖)

讀寫鎖特性:

  • 讀寫鎖是「寫模式加鎖」時,解鎖前,全部對該鎖加鎖的線程都會被阻塞。
  • 讀寫鎖是「讀模式加鎖」時, 若是線程以讀模式對其加鎖會成功;若是線程以寫模式加鎖會阻塞。
  • 讀寫鎖是「讀模式加鎖」時, 既有試圖以寫模式加鎖的線程,也有試圖以讀模式加鎖的線程。那麼讀寫鎖會阻塞隨後的讀模式鎖請求。優先知足寫模式鎖。讀鎖、寫鎖並行阻塞,寫鎖優先級高

讀寫鎖也叫共享-獨佔鎖。當讀寫鎖以讀模式鎖住時,它是以共享模式鎖住的;當它以寫模式鎖住時,它是以獨佔模式鎖住的。寫獨佔、讀共享。

讀寫鎖很是適合於對數據結構讀的次數遠大於寫的狀況。

協程併發

協程:coroutine。也叫輕量級線程。

與傳統的系統級線程和進程相比,協程最大的優點在於「輕量級」。能夠輕鬆建立上萬個而不會致使系統資源衰竭。而線程和進程一般很難超過1萬個。這也是協程別稱「輕量級線程」的緣由。

一個線程中能夠有任意多個協程,但某一時刻只能有一個協程在運行,多個協程分享該線程分配到的計算機資源。

多數語言在語法層面並不直接支持協程,而是經過庫的方式支持,但用庫的方式支持的功能也並不完整,好比僅僅提供協程的建立、銷燬與切換等能力。若是在這樣的輕量級線程中調用一個同步 IO 操做,好比網絡通訊、本地文件讀寫,都會阻塞其餘的併發執行輕量級線程,從而沒法真正達到輕量級線程自己指望達到的目標。

在協程中,調用一個任務就像調用一個函數同樣,消耗的系統資源最少!但能達到進程、線程併發相同的效果。

協程的目的:提升程序執行的效率。

在一次併發任務中,進程、線程、協程都可以實現。從系統資源消耗的角度出發來看,進程至關多,線程次之,協程最少。

進程、線程、協程 總結

  • 進程、線程、協程均可以完成併發。
  • 進程併發穩定性高、開銷大。
  • 線程開銷小(節省資源)。
  • 協程效率高。

選擇哪種進行併發並無一個統一的答案,須要根據具體狀況具體分析。

最後,舉一個例子幫助你們更好的理解一下三者的區別:

好比說我是一家生產手機的老闆,要經過一條生產線(進程)去生產手機,光有生產線還不夠,還須要工人(線程)來完成手機的生產。

這時只有一條生產線,一個工人,咱們能夠看做是一個單進程、單線程的程序。

接下來我發現這樣效率過低了,我要多招工人,假設招了 50 個工人,也就是有了 50 個線程。

這時有一條生產線,50 個工人,咱們能夠看做是一個單進程、多線程的程序。

再接下來,還想提升效率,我弄 10 條生產線,每條生產線招 50 個工人,那麼就須要 500 個工人。

這時有 10 條生產線,500 個工人,咱們能夠看做是一個多進程、多線程的程序。

可是若是上一個程序沒有完成,接下來的工人都會在等待。那麼就會有工人在這時刷刷微博、逛逛朋友圈什麼的。我心想這不行,我得把這空閒時間利用起來啊。因此,老闆又制定了一條規則:全部工人,若是有空閒時間,都要到我家來搬磚(協程)。

這時有 10 條生產線,500 個工人,工做的同時還去我家幫忙搬磚,咱們能夠看做是一個多進程、多線程、多協程的程序。(我可真是個黑心老闆 ...)

Go 併發

Go 在語言級別支持協程,叫 goroutine。Go 語言標準庫提供的全部系統調用操做(包括全部同步IO操做),都會出讓 CPU 給其餘 goroutine。這讓輕量級線程的切換管理不依賴於系統的線程和進程,也不須要依賴於 CPU 的核心數量。

有人把 Go 比做21世紀的 C 語言。第一是由於 Go 語言設計簡單,第二,21世紀最重要的就是並行程序設計,而 Go 從語言層面就支持並行。同時,併發程序的內存管理有時候是很是複雜的,而 Go 語言提供了自動垃圾回收機制。

Go 語言爲併發編程而內置的上層 API 基於順序通訊進程模型 CSP(communicating sequential processes)。這就意味着顯式鎖都是能夠避免的,由於 Go 經過相對安全的通道發送和接受數據以實現同步,這大大地簡化了併發程序的編寫。

Go 語言中的併發程序主要使用兩種手段來實現。goroutine 和 channel。

李培冠博客

歡迎訪問個人我的網站:

李培冠博客:lpgit.com

相關文章
相關標籤/搜索