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


前面已經講過很多Golang系列知識,感興趣的可以看看以前的文章,https://www.cnblogs.com/zhangweizhong/category/1275863.html

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

 

一、並發和並行

Go是並發語言,而不是並行語言。所以我們在討論,我們首先必須了解什么是並發,以及它與並行性有什么不同。

 

什么是並發

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

比如,一個人在晨跑。在晨跑時,他的鞋帶松了。現在這個人停止跑步,系鞋帶,然后又開始跑步。這是一個典型的並發。這個人能夠同時處理跑步和系鞋帶,這是一個人能夠同時處理很多事情。

 

什么是並行

並行就是同一時刻做很多事情。這聽起來可能與並發類似,但實際上是不同的。

再比如,這個人正在慢跑,並且使用他的手機聽音樂。在這種情況下,一個人一邊慢跑一邊聽音樂,那就是他同時在做很多事情。這就是所謂的並行。

 

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

二、協程(Goroutines)

go中使用Goroutines來實現並發。Goroutines是與其他函數或方法同時運行的函數或方法。Goroutines可以被認為是輕量級的線程。與線程相比,創建Goroutine的成本很小。因此,Go應用程序可以並發運行數千個Goroutines。

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實現並發執行。

 

三、通道(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)
    }
}

 

四、緩沖通道

之前學習的所有通道基本上都沒有緩沖。發送和接收到一個未緩沖的通道是阻塞的。

可以用緩沖區創建一個通道。發送到一個緩沖通道只有在緩沖區滿時才被阻塞。類似地,從緩沖通道接收的信息只有在緩沖區為空時才會被阻塞。

可以通過將額外的容量參數傳遞給make函數來創建緩沖通道,該函數指定緩沖區的大小。

語法:

ch := make(chan type, capacity) 

上述語法的容量應該大於0,以便通道具有緩沖區。默認情況下,無緩沖通道的容量為0,因此在之前創建通道時省略了容量參數。

示例代碼:

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

 

五、最后

以上,就把golang並發編程相關的內容介紹完了,希望能對大家有所幫助。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM