前面已經講過很多Golang系列知識,感興趣的可以看看以前的文章,https://www.cnblogs.com/zhangweizhong/category/1275863.html,
接下來要說的是golang的並發,其實之前簡單介紹過協程(goroutine)和管道(channel) 等基礎內容,只是比較簡單,只講了基本的語法。今天就詳細說說golang的並發編程。
一、並發和並行
Go是並發語言,而不是並行語言。所以我們在討論,我們首先必須了解什么是並發,以及它與並行性有什么不同。
什么是並發
並發就是一段時間內處理許多事情。
比如,一個人在晨跑。在晨跑時,他的鞋帶松了。現在這個人停止跑步,系鞋帶,然后又開始跑步。這是一個典型的並發。這個人能夠同時處理跑步和系鞋帶,這是一個人能夠同時處理很多事情。
什么是並行
並行就是同一時刻做很多事情。這聽起來可能與並發類似,但實際上是不同的。
再比如,這個人正在慢跑,並且使用他的手機聽音樂。在這種情況下,一個人一邊慢跑一邊聽音樂,那就是他同時在做很多事情。這就是所謂的並行。

二、協程(Goroutines)
go中使用Goroutines來實現並發。Goroutines是與其他函數或方法同時運行的函數或方法。Goroutines可以被認為是輕量級的線程。與線程相比,創建Goroutine的成本很小。因此,Go應用程序可以並發運行數千個Goroutines。
Goroutines在線程上的優勢。
-
與線程相比,Goroutines非常便宜。它們只是堆棧大小的幾個kb,堆棧可以根據應用程序的需要增長和收縮,而在線程的情況下,堆棧大小必須指定並且是固定的
-
Goroutines被多路復用到較少的OS線程。在一個程序中可能只有一個線程與數千個Goroutines。如果線程中的任何Goroutine都表示等待用戶輸入,則會創建另一個OS線程,剩下的Goroutines被轉移到新的OS線程。所有這些都由運行時進行處理,我們作為程序員從這些復雜的細節中抽象出來,並得到了一個與並發工作相關的干凈的API。
-
當使用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並發編程相關的內容介紹完了,希望能對大家有所幫助。