Golang 入門系列(十五)如何理解go的併發?

前面已經講過不少Golang系列知識,感興趣的能夠看看之前的文章,https://www.cnblogs.com/zhangweizhong/category/1275863.htmlcss

接下來要說的是golang的併發,其實以前簡單介紹過協程(goroutine)和管道(channel) 等基礎內容,只是比較簡單,只講了基本的語法。今天就詳細說說golang的併發編程。html

 

1、併發和並行

Go是併發語言,而不是並行語言。因此咱們在討論,咱們首先必須瞭解什麼是併發,以及它與並行性有什麼不一樣。程序員

 

什麼是併發

併發就是一段時間內處理許多事情。golang

好比,一我的在晨跑。在晨跑時,他的鞋帶鬆了。如今這我的中止跑步,繫鞋帶,而後又開始跑步。這是一個典型的併發。這我的可以同時處理跑步和繫鞋帶,這是一我的可以同時處理不少事情。編程

 

什麼是並行

並行就是同一時刻作不少事情。這聽起來可能與併發相似,但其實是不一樣的。緩存

再好比,這我的正在慢跑,而且使用他的手機聽音樂。在這種狀況下,一我的一邊慢跑一邊聽音樂,那就是他同時在作不少事情。這就是所謂的並行。安全

 

併發不是並行。併發更關注的是程序的設計層面,併發的程序徹底是能夠順序執行的,只有在真正的多核CPU上纔可能真正地同時運行。並行更關注的是程序的運行層面,並行通常是簡單的大量重複,例如GPU中對圖像處理都會有大量的並行運算。爲更好的編寫併發程序,從設計之初Go語言就注重如何在編程語言層級上設計一個簡潔安全高效的抽象模型,讓程序員專一於分解問題和組合方案,並且不用被線程管理和信號互斥這些繁瑣的操做分散精力。
 
上圖能清楚的說明了併發和並行的區別。
 

2、協程(Goroutines)

go中使用Goroutines來實現併發。Goroutines是與其餘函數或方法同時運行的函數或方法。Goroutines能夠被認爲是輕量級的線程。與線程相比,建立Goroutine的成本很小。所以,Go應用程序能夠併發運行數千個Goroutines。bash

Goroutines在線程上的優點。併發

  1. 與線程相比,Goroutines很是便宜。它們只是堆棧大小的幾個kb,堆棧能夠根據應用程序的須要增加和收縮,而在線程的狀況下,堆棧大小必須指定而且是固定的編程語言

  2. Goroutines被多路複用到較少的OS線程。在一個程序中可能只有一個線程與數千個Goroutines。若是線程中的任何Goroutine都表示等待用戶輸入,則會建立另外一個OS線程,剩下的Goroutines被轉移到新的OS線程。全部這些都由運行時進行處理,咱們做爲程序員從這些複雜的細節中抽象出來,並獲得了一個與併發工做相關的乾淨的API。

  3. 當使用Goroutines訪問共享內存時,經過設計的通道能夠防止競態條件發生。通道能夠被認爲是Goroutines通訊的管道。

 

如何使用Goroutines

在函數或方法調用前面加上關鍵字go,您將會同時運行一個新的Goroutine。

實例代碼:

package main

import (
    "fmt"
    "time"
)

func hello() {
    fmt.Println("Hello world goroutine")
}
func main() {
    go hello()
    time.Sleep(1 * time.Second)
    fmt.Println("main function")
}
運行結果: Hello world goroutine main function

 

如何啓動多個Goroutines

示例代碼:

package main

import (
    "fmt"
    "time"
)

func numbers() {
    for i := 1; i <= 5; i++ {
        time.Sleep(250 * time.Millisecond)
        fmt.Printf("%d ", i)
    }
}
func alphabets() {
    for i := 'a'; i <= 'e'; i++ {
        time.Sleep(400 * time.Millisecond)
        fmt.Printf("%c ", i)
    }
}
func main() {
    go numbers()
    go alphabets()
    time.Sleep(3000 * time.Millisecond)
    fmt.Println("main terminated")
}
運行結果:

1 a 2 3 b 4 c 5 d e main terminated

 

Goroutine切換

下面經過素數計算的例子來講明goland是如何經過切換不一樣的goroutine實現併發的。

package main

import (
"fmt"
"runtime"
"sync"
)

var wg sync.WaitGroup

func main() {

runtime.GOMAXPROCS(1)

wg.Add(2)
go printPrime("A")
go printPrime("B")

fmt.Println("Wait for finish")
wg.Wait()
fmt.Println("Program End")
}

func printPrime(prefix string) {
defer wg.Done()

  nextNum:
for i := 2; i < 6000; i++ {
for j := 2; j < i; j++ {
if i%j == 0 {
continue nextNum
}
}
fmt.Printf("%s:%d\n", prefix, i)
}
fmt.Printf("complete %s\n", prefix)
}

運行結果:
Wait for finish B:2 B:3 B:5 B:7 B:11 ... B:457 B:461 B:463 B:467 A:2 A:3 A:5 A:7 ... A:5981 A:5987 complete A B:5939 B:5953 B:5981 B:5987 complete B Program End

經過以上的輸出結果,能夠看出兩個Goroutine是在一個處理器上經過切換goroutine實現併發執行。

 

3、通道(channels)

通道能夠被認爲是Goroutines通訊的管道。相似於管道中的水從一端到另外一端的流動,數據能夠從一端發送到另外一端,經過通道接收。

 

聲明通道

每一個通道都有與其相關的類型。該類型是通道容許傳輸的數據類型。(通道的零值爲nil。nil通道沒有任何用處,所以通道必須使用相似於地圖和切片的方法來定義。)

示例代碼:

package main

import "fmt"

func main() {
    var a chan int
    if a == nil {
        fmt.Println("channel a is nil, going to define it")
        a = make(chan int)
        fmt.Printf("Type of a is %T", a)
    }
}
運行結果:

channel a is nil, going to define it
Type of a is chan int

也能夠簡短的聲明:

a := make(chan int)

發送和接收

發送和接收的語法:

data := <- a   // read from channel a
a <- data      // write to channel a

在通道上箭頭的方向指定數據是發送仍是接收。

 

一個通道發送和接收數據,默認是阻塞的。當一個數據被髮送到通道時,在發送語句中被阻塞,直到另外一個Goroutine從該通道讀取數據。相似地,當從通道讀取數據時,讀取被阻塞,直到一個Goroutine將數據寫入該通道。

這些通道的特性是幫助Goroutines有效地進行通訊,而無需像使用其餘編程語言中很是常見的顯式鎖或條件變量。

示例代碼:

package main

import (
    "fmt"
    "time"
)

func hello(done chan bool) {
    fmt.Println("hello go routine is going to sleep")
    time.Sleep(4 * time.Second)
    fmt.Println("hello go routine awake and going to write to done")
    done <- true
}
func main() {
    done := make(chan bool)
    fmt.Println("Main going to call hello go goroutine")
    go hello(done)
    <-done
    fmt.Println("Main received data")
}

運行結果:

 Main going to call hello go goroutine
 hello go routine is going to sleep
 hello go routine awake and going to write to done
 Main received data

 

定向通道

以前咱們學習的通道都是雙向通道,咱們能夠經過這些通道接收或者發送數據。咱們也能夠建立單向通道,這些通道只能發送或者接收數據。

建立僅能發送數據的通道,示例代碼:

package main

import "fmt"

func sendData(sendch chan<- int) {
    sendch <- 10
}

func main() {
    sendch := make(chan<- int)
    go sendData(sendch)
    fmt.Println(<-sendch)
}

報錯:

 # command-line-arguments
 .\main.go:12:14: invalid operation: <-sendch (receive from send-only type chan<- int)

 

示例代碼:

package main

import "fmt"

func sendData(sendch chan<- int) {
    sendch <- 10
}

func main() {
    chnl := make(chan int)
    go sendData(chnl)
    fmt.Println(<-chnl)
}

運行結果:
10

 

死鎖

爲何會死鎖?非緩衝信道上若是發生了流入無流出,或者流出無流入,也就致使了死鎖。或者這樣理解 Go啓動的全部goroutine裏的非緩衝信道必定要一個線裏存數據,一個線裏取數據,要成對才行 。

示例代碼:

package main

func main() {
c, quit := make(chan int), make(chan int)

go func() {
c <- 1 // c通道的數據沒有被其餘goroutine讀取走,堵塞當前goroutine
quit <- 0 // quit始終沒有辦法寫入數據
}()

<-quit // quit 等待數據的寫
}
 報錯: fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan send]: main.main() /tmp/sandbox249677995/main.go:11 +0x80

 

關閉通道

關閉通道只是關閉了向通道寫入數據,但能夠從通道讀取。

package main

import (
    "fmt"
)

var ch chan int = make(chan int, 3)

func main() {
    ch <- 1
    ch <- 2
    ch <- 3

    close(ch)
    
    for v := range ch {
        fmt.Println(v)
    }
}

 

4、緩衝通道

以前學習的全部通道基本上都沒有緩衝。發送和接收到一個未緩衝的通道是阻塞的。

能夠用緩衝區建立一個通道。發送到一個緩衝通道只有在緩衝區滿時才被阻塞。相似地,從緩衝通道接收的信息只有在緩衝區爲空時纔會被阻塞。

能夠經過將額外的容量參數傳遞給make函數來建立緩衝通道,該函數指定緩衝區的大小。

語法:

ch := make(chan type, capacity) 

上述語法的容量應該大於0,以便通道具備緩衝區。默認狀況下,無緩衝通道的容量爲0,所以在以前建立通道時省略了容量參數。

示例代碼:

func main() { done := make(chan int, 1) // 帶緩存的管道 go func(){ fmt.Println("你好, 世界") done <- 1 }() <-done }

 

5、最後

以上,就把golang併發編程相關的內容介紹完了,但願能對你們有所幫助。

相關文章
相關標籤/搜索