併發編程對於任何語言來講都不是一件簡單的事情。Go在設計之初主打高併發,爲使用者提供了goroutine,使用的方式雖然簡單,可是用好卻不是那麼容易,咱們一塊兒來學習Go中的併發編程。web
並行(parallel): 指在同一時刻,有多條指令在多個處理器上同時執行。編程
併發(concurrency): 指在同一時刻只能有一條指令執行,但多個進程指令被快速的輪換執行,使得在宏觀上具備多個進程同時執行的效果,但在微觀上並非同時執行的,只是把時間分紅若干段,經過cpu時間片輪轉使多個進程快速交替的執行。數組
進程: 咱們在操做系統中的每一次操做至關於觸發了一個進程,打開一個瀏覽器,點開任務管理器,等等。瀏覽器
線程: 輕量級的進程,本質還是進程 。獨立地址空間,擁有PCB,最小分配資源單位,可當作是隻有一個線程的進程;而線程是程序的最小的執行單位,有獨立的PCB,線程擁有本身的棧空間,但沒有獨立的地址空間 ,共享當前進程的地址空間。緩存
對操做系統來講,線程是最小的執行單元,進程是最小的資源管理單元。安全
不管進程仍是線程,都是由操做系統所管理的。多線程
協程: coroutine,協同式程序。協程不是輕量級的線程, 協程與線程的關係並不像是線程與進程的關係。併發
先了解一些概念:異步
在每一個任務運行前, CPU 都須要知道任務從哪裏加載,又從哪裏開始運行。也就是說,須要系統事先給他設置好 CPU 寄存器和程序計數器(Program Counter,PC)svg
CPU 寄存器:是 CPU 內置的容量小、但速度極快的內存
程序計數器:是用來存儲 CPU 正在執行的指令位置、或者即將執行的下一條指令位置
它們都是 CPU 在運行任何任務前,必須依賴的環境,所以也被叫作 CPU 上下文。
上下文切換:就是先把前一個任務的 CPU 上下文(也就是 CPU 寄存器和程序計數器)保存起來,而後加載新任務的上下文到這些寄存器和程序計數器,最後再跳轉到程序計數器所指的新位置,運行新任務。
根據任務的不一樣,又分爲進程上下文切換
、線程上下文切換
、中斷上下文切換
。
進程的運行態:
Linux 按照特權等級,把進程的運行空間分爲內核空間
和用戶空間
。在這兩種空間中運行的進程狀態分別稱爲內核態
和用戶態
。
系統調用:
從用戶態到內核態的轉變,須要經過系統調用來完成。好比查看文件時,須要執行屢次系統調用:open、read、write、close等。系統調用的過程以下:
因此,一次系統調用的過程,實際上是發生了兩次 CPU 上下文切換。
什麼是進程上下文切換:
進程上下文切換和系統調用的區別:
線程是調度的基本單位,而進程則是資源擁有的基本單位。 當進程只有一個線程時,能夠認爲進程就等於線程。 當進程擁有多個線程時,這些線程會共享相同的虛擬內存和全局變量等資源。這些資源在上下文切換時是不須要修改的。線程也有本身的私有數據,好比棧和寄存器等,這些在上下文切換時也是須要保存的。
線程上下文切換有兩種狀況:
中斷處理會打斷進程的正常調度和執行。在打斷其餘進程時,須要將進程當前的狀態保存下來,中斷結束後,進程仍然能夠從原來的狀態恢復運行。
中斷上下文切換並不涉及到進程的用戶態。因此,即使中斷過程打斷了一個正處在用戶態的進程,也不須要保存和恢復這個進程的虛擬內存、全局變量等用戶態資源。中斷上下文,其實只包括內核態中斷服務程序執行所必須的狀態,包括 CPU 寄存器、內核堆棧、硬件中斷參數等。
在有線程的前提下,提出來協程,它到底解決了什麼問題呢?
咱們知道線程的出現是爲了減少進程的切換開銷,提升多核的利用率。當程序運行到某個IO發送阻塞的時候,能夠切換到其餘線程去執行,這樣不會浪費CPU時間。而線程的切換徹底是經過操做系統去完成的,切換的時候通常會經過系統從用戶態切換到內核態。這段話的重點是,線程是內核態的。
咱們常見的代碼邏輯都是被封裝在一個個函數塊裏面。每次傳遞一個參數,這個函數就會從頭至尾執行一遍,有對應的輸出。若是在執行的過程當中,發生了線程的搶佔切換,那麼當前線程就會保存函數當前的上下文信息(放到寄存器裏面),去執行其餘線程的邏輯。當這個線程從新執行時會根據以前保存的上下文信息繼續執行。這段話的重點是,線程的切換須要保存函數的上下文信息。
並且現代操做系統通常都是搶佔式的,因此多個線程在執行的時候在何時切換咱們是沒法控制的。因此,多線程編程時爲了保證數據的準確性與安全性,咱們常常須要加鎖。這段話的重點是,線程的執行順序咱們沒法控制,何時切換咱們也幾乎沒法控制。
因爲線程在運行時常常會因爲IO阻塞(或者時鐘阻塞)而放棄CPU,會致使咱們的邏輯不能流暢的執行下去。因此,咱們通常採用異步+回調的方式去執行代碼。當線程與到阻塞時直接返回,繼續執行下面的邏輯。同時註冊一個回調函數,當內核數據準備好了以後再通知咱們。這種寫代碼的方式其實不夠直觀,由於咱們通常都習慣順序執行的邏輯,一段代碼能從頭跑到尾那是再理想不過了。這段話的重點是,涉及到IO阻塞的多線程編程時,咱們通常用異步+回調的方式來解決問題。
協程是用戶態的,他是包含在線程裏面的,簡答來講你能夠認爲一個線程能夠按照你的規則把本身的時間片分給多個協程去執行。
由於一個線程裏面可能有多個協程,因此協程的執行須要切換,切換就須要保存當前的上下文信息(一組寄存器和調用堆棧,保存在自身的用戶空間內),這樣才能在再次執行的時候繼續前面的工做。相比線程,協程要保存的東西都不多。
相比線程,協程的切換時機是能夠控制的。咱們能夠告訴協程,代碼執行到哪句的時候切換到哪一個協程,這樣就能夠避免線程執行不肯定性帶來的安全問題,避免了各類鎖機制帶來的相關問題。
協程的代碼看起來是同步的,不須要回調。好比說有兩個協程,A協程執行到第3句就必定會切換到B協程的第4句,假如A與B裏面都有循環,那展開來看其實就是A與B函數不斷的順序執行。這種感受有點像併發,一樣在一個線程上的A與B不斷的切換去執行邏輯。
協程不過是一種用戶級別的實現手段,他並不像線程那樣有明確的概念與實體,更像是一個語言技巧。他的切換開銷很小。
協程擁有本身的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其餘地方,在切回來的時候,恢復先前保存的寄存器上下文和棧。所以:協程能保留上一次調用時的狀態(即全部局部狀態的一個特定組合),每次過程重入時,就至關於進入上一次調用的狀態,換種說法:進入上一次離開時所處邏輯流的位置。在併發編程中,協程與線程相似,每一個協程表示一個執行單元,有本身的本地數據,與其它協程共享全局數據和其它資源。目前主流語言基本上都選擇了多線程做爲併發設施,與線程相關的概念是搶佔式多任務(Preemptive multitasking),而與協程相關的是協做式多任務。
無論是進程仍是線程,每次阻塞、切換都須要陷入系統調用(system call),先讓CPU跑操做系統的調度程序,而後再由調度程序決定該跑哪個進程(線程)。並且因爲搶佔式調度執行順序沒法肯定的特色,使用線程時須要很是當心地處理同步問題,而協程徹底不存在這個問題(事件驅動和異步程序也有一樣的優勢)。
協做式的任務,是要用戶本身來負責任務的讓出的。若是一個任務不主動讓出,其餘任務就不會獲得調度。這是協程的一個弱點,可是好好的規劃,這實際上是一個能夠變得很強大的優勢。
總結一下上面的重點:
1.多線程處理,叫作搶佔式多任務處理;多協程處理,叫作協做式多任務處理。
2.歷史上是先有協程,可是由於它是非搶佔式的,致使多任務時間片不能公平分享,因此後來所有廢棄了協程改爲搶佔式的線程。
3.協程是用戶態的,是包含在一個線程裏面的多個執行單元。意味他是單線程處理的過程。(好比你的main函數比協程修飾的函數先中止,那麼協程是沒有執行完的)協程都沒有參與多核CPU的並行處理。而線程是在多核 CPU上是受操做系統調度並行執行的。
4.因爲協程能夠在用戶空間內切換上下文,再也不須要陷入內核來作線程切換,避免了大量的用戶空間和內核空間之間的數據拷貝,下降了CPU的消耗,從而避免了追求高併發時CPU早早到達瓶頸的窘境 。
5.協程本質仍是單線程下處理多任務,單線程的瓶頸也是協程的瓶頸。我以爲協程最大的意義就是能夠用同步方式編寫異步代碼 。
goroutine是Go並行設計的核心。 通常會使用goroutine來處理併發任務 。goroutine是go語言中最爲NB的設計,也是其魅力所在,goroutine的本質是協程,是實現並行計算的核心。它是處於異步方式運行,你不須要等它運行完成之後在執行之後的代碼。
Goroutine是創建在線程之上的輕量級的抽象。它容許咱們以很是低的代價在同一個地址空間中並行地執行多個函數或者方法。相比於線程,它的建立和銷燬的代價要小不少,而且它的調度是獨立於線程的。在Go中建立一個goroutine很是簡單,使用「go」關鍵字便可:
go hello(str)
先來看一個簡單的例子:
package main import ( "time" ) func Print() { for i := 1; i <= 5; i++ { time.Sleep(100 * time.Millisecond) println(i) } } func HelloWorld() { println("Hello world") } func main() { go Print() // 開啓第一個goroutine go HelloWorld() // 開啓第二個goroutine time.Sleep(2*time.Second) println("end") } 打印: Hello world 1 2 3 4 5 end
CountDownLatch是Java中的一個同步輔助類,在完成一組正在其餘線程中執行的操做以前,它容許一個或多個線程一直等待。
在Go中可使用sync包中的WaitGroup來實現同樣的功能,WaitGroup 等待一組goroutinue執行完畢,主程序調用 Add 添加等待的goroutinue數量,每一個goroutinue在執行結束時調用 Done ,此時等待隊列數量減1,主程序經過Wait阻塞,直到等待隊列爲0。
package main import ( "fmt" "sync" ) func cal(a int , b int ,n *sync.WaitGroup) { c := a+b fmt.Printf("%d + %d = %d\n",a,b,c) defer n.Done() //goroutinue完成後, WaitGroup的計數-1 } func main() { var go_sync sync.WaitGroup //聲明一個WaitGroup變量 for i :=0 ; i<10 ;i++{ go_sync.Add(1) // WaitGroup的計數加1 go cal(i,i+1,&go_sync) } go_sync.Wait() //等待全部goroutine執行完畢 println("主程序執行完畢") } 結果: 0 + 1 = 1 1 + 2 = 3 9 + 10 = 19 3 + 4 = 7 4 + 5 = 9 2 + 3 = 5 5 + 6 = 11 6 + 7 = 13 7 + 8 = 15 8 + 9 = 17 主程序執行完畢
channel用於數據傳遞或數據共享,其本質是一個先進先出的隊列,使用goroutine+channel進行數據通信簡單高效,同時也線程安全,多個goroutine可同時修改一個channel,不須要加鎖。
channel可分爲三種類型:
注意,必須使用make 建立channel:
c1 := make(chan int) c2 := make(chan string)
channel經過操做符<-
來接收和發送數據 :
c1 <- str //發送數據str到c1 newStr := <- c1 //從str中接受數據並賦值給newStr
默認的,信道的存消息和取消息都是阻塞的 , 叫作無緩衝的信道。也就是說, 無緩衝的信道在取消息和存消息的時候都會掛起當前的goroutine,除非另外一端已經準備好。
那麼有緩存的channel是指在聲明的時候指定該channel緩存的容量:
ch := make(chan int, 10)
有緩存的 channel 相似一個阻塞隊列(採用環形數組實現)。當緩存未滿時,向 channel 中發送消息時不會阻塞,當緩存滿時,發送操做將被阻塞,直到有其餘 goroutine 從中讀取消息;相應的,當 channel 中消息不爲空時,讀取消息不會出現阻塞,當 channel 爲空時,讀取操做會形成阻塞,直到有 goroutine 向 channel 中寫入消息。
ch := make(chan int, 3) // 讀消息阻塞,由於channel爲空 <- ch ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 // 存入消息阻塞,channel已滿,未被讀取 ch <- 4
channel的使用:
1.使用channel阻塞主線程,直到子goroutine完成才繼續往下走。
c := make(chan int) go func(){ //do something c <- 1 }() doAnnotherThing() <- c
2.消息傳遞
func test(){ intChan := make(chan int) go func() { intChan <- 1 }() value := <- intChan fmt.Println("value : ", value) }
匿名函數中的操做產生一個值,將該值傳遞到主函數中去。
3.合併多個channel的輸出
package main import ( "fmt" "time" ) func testMergeInput() { input1 := make(chan int) input2 := make(chan int) output := make(chan int) //將 channel 1 和 2 中的數據輸出到output中 go func(in1, in2 <-chan int, out chan<- int) { for { select { case v := <-in1: out <- v case v := <-in2: out <- v } } }(input1, input2, output) go func() { for i := 0; i < 10; i++ { input1 <- i time.Sleep(time.Millisecond * 100) } }() go func() { for i := 20; i < 30; i++ { input2 <- i time.Sleep(time.Millisecond * 100) } }() go func() { for { select { case value := <-output: fmt.Println("輸出:", value) } } }() time.Sleep(time.Second * 5) fmt.Println("主線程退出") } func main(){ testMergeInput() }
4.模擬生產者和消費者模式
package main import ( "fmt" "math/rand" "time" ) var( lockChan = make(chan int, 1) remainMoney = 1000 ) func testSynchronize() { quit := make(chan bool, 2) go func() { for i:=0; i<10;i++{ money := (rand.Intn(12) + 1) * 100 go testSynchronize_expense(money) time.Sleep(time.Millisecond * time.Duration(rand.Intn(500))) } quit <- true }() go func() { for i:=0; i<10; i++{ money := (rand.Intn(12) + 1) * 100 go testSynchronize_gain(money) time.Sleep(time.Millisecond * time.Duration(rand.Intn(500))) } quit <- true }() <- quit <- quit fmt.Println("主程序退出") } func testSynchronize_expense(money int) { lockChan <- 0 if(remainMoney >= money){ srcRemainMoney := remainMoney remainMoney -= money fmt.Printf("原來有%d, 花了%d,剩餘%d\n", srcRemainMoney, money, remainMoney) }else{ fmt.Printf("想消費%d錢不夠了, 只剩%d\n", money, remainMoney) } <- lockChan } func testSynchronize_gain(money int) { lockChan <- 0 srcRemainMoney := remainMoney remainMoney += money fmt.Printf("原來有%d, 賺了%d,剩餘%d\n", srcRemainMoney, money, remainMoney) <- lockChan } func main(){ testSynchronize() }