1. 程式人生 > >golang並發(1)介紹

golang並發(1)介紹

依然 .... 內置 htm 子進程 進程 closed 進入 times

概述

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

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

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

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

技術分享圖片

並行和並發

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

技術分享圖片

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

技術分享圖片

大師曾以咖啡機的例子來解釋並行和並發的區別。

技術分享圖片

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

常見並發編程技術

進程並發

程序和進程

程序,是指編譯好的二進制文件,在磁盤上,不占用系統資源

(cpu、內存、打開的文件、設備、鎖....)

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

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

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

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

windows系統下,通過查看“任務管理器”,可以查看相應的進程。包括我們在基礎班寫的“飛機大戰”等程序,運行起來後也可以在“任務管理器”中查看到。運行起來的程序就是一個進程。如下圖所示:

技術分享圖片

進程狀態

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

技術分享圖片

進程並發

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

1:系統開銷比較大,占用資源比較多,開啟進程數量比較少。

2:在unix/linux系統下,還會產生“孤兒進程”和“僵屍進程”。

通過前面查看操作系統的進程信息,我們知道在操作系統中,可以產生很多的進程。在unix/linux系統中,正常情況下,子進程是通過父進程fork創建的,子進程再創建新的進程。

並且父進程永遠無法預測子進程 到底什麽時候結束。 當一個 進程完成它的工作終止之後,它的父進程需要調用系統調用取得子進程的終止狀態

孤兒進程

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

僵屍進程

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

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

線程並發

什麽是線程

LWPlight weight process 輕量級的進程,本質仍是進程 (Linux)

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

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

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

線程:最小的執行單位

進程:最小分配資源單位,可看成是只有一個線程的進程。

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

技術分享圖片

線程同步

同步即協同步調,按預定的先後次序運行。

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

舉例1 銀行存款 5000。櫃臺,折:取3000;提款機,卡:取 3000。剩余:2000

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

產生的現象叫做“與時間有關的錯誤”(time related)。為了避免這種數據混亂,線程需要同步。

“同步”的目的,是為了避免數據混亂,解決與時間有關的錯誤。實際上,不僅線程間需要同步,進程間、信號間等等都需要同步機制。

因此,所有“多個控制流,共同操作一個共享資源”的情況,都需要同步。

鎖的應用

互斥量mutex

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

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

資源還是共享的,線程間也還是競爭的,

但通過“鎖”就將資源的訪問變成互斥操作,而後與時間有關的錯誤也不會再產生了。

技術分享圖片

但,應註意:同一時刻,只能有一個線程持有該鎖。

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

所以,互斥鎖實質上是操作系統提供的一把“建議鎖”(又稱“協同鎖”),建議程序中有多線程訪問共享資源的時候使用該機制。但,並沒有強制限定。

因此,即使有了mutex,如果有線程不按規則來訪問數據,依然會造成數據混亂。

讀寫鎖

與互斥量類似,但讀寫鎖允許更高的並行性。其特性為:寫獨占,讀共享。

l 讀寫鎖狀態:

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

1. 讀模式下加鎖狀態 (讀鎖)

2. 寫模式下加鎖狀態 (寫鎖)

l 讀寫鎖特性:

  1. 讀寫鎖是“寫模式加鎖”時, 解鎖前,所有對該鎖加鎖的線程都會被阻塞。
  2. 讀寫鎖是“讀模式加鎖”時, 如果線程以讀模式對其加鎖會成功;如果線程以寫模式加鎖會阻塞。
  3. 讀寫鎖是“讀模式加鎖”時, 既有試圖以寫模式加鎖的線程,也有試圖以讀模式加鎖的線程。那麽讀寫鎖會阻塞隨後的讀模式鎖請求。優先滿足寫模式鎖。讀鎖、寫鎖並行阻塞,寫鎖優先級高

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

讀寫鎖非常適合於對數據結構讀的次數遠大於寫的情況。

協程並發

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

與傳統的系統級線程和進程相比,協程最大的優勢在於“輕量級”。可以輕松創建上萬個而不會導致系統資源衰竭。而線程和進程通常很難超過1萬個。這也是協程別稱輕量級線程的原因。

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

多數語言在語法層面並不直接支持協程,而是通過庫的方式支持,但用庫的方式支持的功能也並不完整,比如僅僅提供協程的創建、銷毀與切換等能力。如果在這樣的輕量級線程中調用一個同步 IO 操作,比如網絡通信、本地文件讀寫,都會阻塞其他的並發執行輕量級線程,從而無法真正達到輕量級線程本身期望達到的目標。

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

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

Go並發

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

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

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

Go語言中的並發程序主要使用兩種手段來實現。goroutinechannel

Goroutine

什麽是Goroutine

goroutineGo並行設計的核心。goroutine說到底其實就是協程,它比線程更小,十幾個goroutine可能體現在底層就是五六個線程,Go語言內部幫你實現了這些goroutine之間的內存共享。執行goroutine只需極少的棧內存(大概是4~5KB),當然會根據相應的數據伸縮。也正因為如此,可同時運行成千上萬個並發任務。goroutinethread更易用、更高效、更輕便。

一般情況下,一個普通計算機跑幾十個線程就有點負載過大了,但是同樣的機器卻可以輕松地讓成百上千個goroutine進行資源競爭。

Goroutine的創建

只需在函數調?語句前添加 go 關鍵字,就可創建並發執?單元。開發?員無需了解任何執?細節,調度器會自動將其安排到合適的系統線程上執行。

在並發編程中,我們通常想將一個過程切分成幾塊,然後讓每個goroutine各自負責一塊工作,當一個程序啟動時,主函數在一個單獨的goroutine中運行,我們叫它main goroutine。新的goroutine會用go語句來創建。而go語言的並發設計,讓我們很輕松就可以達成這一目的。

示例代碼:

package main

import (
    "fmt"
    "time"
)

func newTask() {
    i := 0
    for {
        i++
        fmt.Printf("new goroutine: i = %d\n", i)
        time.Sleep(1 * time.Second) //延時1s
    }
}

func main() {
    //創建一個 goroutine,啟動另外一個任務
    go newTask()
    i := 0
    //main goroutine 循環打印
    for {
        i++
        fmt.Printf("main goroutine: i = %d\n", i)
        time.Sleep(1 * time.Second) //延時1s
    }
}

  

程序運行結果:

技術分享圖片

Goroutine特性

goroutine退出後,其它的工作goroutine也會自動退出:

技術分享圖片
 1 package main
 2 
 3 import (
 4 "fmt"
 5 "time"
 6 )
 7 
 8 func newTask() {
 9     i := 0
10     for {
11         i++
12         fmt.Printf("new goroutine: i = %d\n", i)
13         time.Sleep(1 * time.Second) //延時1s
14     }
15 }
16 
17 func main() {
18     //創建一個 goroutine,啟動另外一個任務
19     go newTask()
20 
21     fmt.Println("main goroutine exit")
22 }
View Code

程序運行結果:

技術分享圖片

runtime

Gosched

runtime.Gosched() 用於讓出CPU時間片,讓出當前goroutine的執行權限,調度器安排其他等待的任務運行,並在下次再獲得cpu時間輪片的時候,從該出讓cpu的位置恢復執行。

有點像跑接力賽,A跑了一會碰到代碼runtime.Gosched() 就把接力棒交給B了,A歇著了,B繼續跑。

示例代碼:

技術分享圖片
 1 package main
 2 
 3 import (
 4 "fmt"
 5 "runtime"
 6 )
 7 
 8 func main() {
 9     //創建一個goroutine
10     go func(s string) {
11         for i := 0; i < 2; i++ {
12             fmt.Println(s)
13         }
14     }("world")
15 
16     for i := 0; i < 2; i++ {
17         runtime.Gosched()  //import "runtime" 包
18         /*
19             屏蔽runtime.Gosched()運行結果如下:
20                 hello
21                 hello
22 
23             沒有runtime.Gosched()運行結果如下:
24                 world
25                 world
26                 hello
27                 hello
28         */
29         fmt.Println("hello")
30     }
31 }
View Code

以上程序的執行過程如下:

主協程進入main()函數,進行代碼的執行。當執行到go func()匿名函數時,創建一個新的協程,開始執行匿名函數中的代碼,主協程繼續向下執行,執行到runtime.Gosched( )時會暫停向下執行,直到其它協程執行完後,再回到該位置,主協程繼續向下執行。

Goexit

調用 runtime.Goexit() 將立即終止當前 goroutine 執?,調度器確保所有已註冊 defer延遲調用被執行。

示例代碼:

技術分享圖片
 1 package main
 2 
 3 import (
 4 "fmt"
 5 "runtime"
 6 )
 7 
 8 func main() {
 9     go func() {
10         defer fmt.Println("A.defer")
11 
12         func() {
13             defer fmt.Println("B.defer")
14             runtime.Goexit() // 終止當前 goroutine, import "runtime"
15             fmt.Println("B") // 不會執行
16         }()
17 
18         fmt.Println("A") // 不會執行
19     }()     //不要忘記()
20 
21     //死循環,目的不讓主goroutine結束
22     for {
23     }
24 }
View Code

程序運行結果:

技術分享圖片

GOMAXPROCS

調用 runtime.GOMAXPROCS() 用來設置可以並行計算的CPU核數的最大值,並返回之前的值。

示例代碼:

技術分享圖片
 1 package main
 2 
 3 import (
 4     "fmt"
 5 )
 6 
 7 func main() {
 8 //n := runtime.GOMAXPROCS(1)     // 第一次 測試
 9 //打印結果:111111111111111111110000000000000000000011111...
10 
11 n := runtime.GOMAXPROCS(2)         // 第二次 測試
12 //打印結果:010101010101010101011001100101011010010100110...
13     fmt.Printf("n = %d\n", n)
14 
15     for {
16         go fmt.Print(0)
17         fmt.Print(1)
18     }
19 }
View Code

在第一次執行runtime.GOMAXPROCS(1) 時,最多同時只能有一個goroutine被執行。所以會打印很多1。過了一段時間後,GO調度器會將其置為休眠,並喚醒另一個goroutine,這時候就開始打印很多0了,在打印的時候,goroutine是被調度到操作系統線程上的。

在第二次執行runtime.GOMAXPROCS(2) 時, 我們使用了兩個CPU,所以兩個goroutine可以一起被執行,以同樣的頻率交替打印01

golang並發(1)介紹