前面已經講過不少Golang系列知識,感興趣的能夠看看之前的文章,https://www.cnblogs.com/zhangweizhong/category/1275863.html,css
接下來要說的是golang的併發,其實以前簡單介紹過協程(goroutine)和管道(channel) 等基礎內容,只是比較簡單,只講了基本的語法。今天就詳細說說golang的併發編程。html
1、併發和並行
Go是併發語言,而不是並行語言。因此咱們在討論,咱們首先必須瞭解什麼是併發,以及它與並行性有什麼不一樣。程序員
什麼是併發
併發就是一段時間內處理許多事情。golang
好比,一我的在晨跑。在晨跑時,他的鞋帶鬆了。如今這我的中止跑步,繫鞋帶,而後又開始跑步。這是一個典型的併發。這我的可以同時處理跑步和繫鞋帶,這是一我的可以同時處理不少事情。編程
什麼是並行
並行就是同一時刻作不少事情。這聽起來可能與併發相似,但其實是不一樣的。緩存
再好比,這我的正在慢跑,而且使用他的手機聽音樂。在這種狀況下,一我的一邊慢跑一邊聽音樂,那就是他同時在作不少事情。這就是所謂的並行。安全
2、協程(Goroutines)
go中使用Goroutines來實現併發。Goroutines是與其餘函數或方法同時運行的函數或方法。Goroutines能夠被認爲是輕量級的線程。與線程相比,建立Goroutine的成本很小。所以,Go應用程序能夠併發運行數千個Goroutines。bash
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實現併發執行。
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併發編程相關的內容介紹完了,但願能對你們有所幫助。
原文出處:https://www.cnblogs.com/zhangweizhong/p/11447334.html