并發編程
- 概述
- 基本概念
- go語言的并發優勢
- goroutine
- goroutine是什么
- 創建goroutine
- 如果主goroutine退出
- runtime包
- Gosched
- Goexit
- GOMAXPROCS
- channel
- 無緩沖的channel
- 有緩沖的channel
- range和close
- 單向channel
- 定時器
- Timer
- Ticker
- Select
- 超時
概述
基本概念
并行和并發概念
- 并行 :在同一時刻 有多條指令在多個編譯器上
- 并發 :在同一時刻 只能有一條指令執行 但是多個進程指令被快速的輪換執行 使得在宏觀上有多個進程被同時執行的效果
如果我們把它具象化成現實中的概念
- 并行就是同一時刻 兩個隊列使用兩臺咖啡機
- 并發就是同一時刻 兩個隊列使用一臺咖啡機
go語言的并發優勢
第一 Go語言在語言層面上天然支持并發 (不像某個語言 23版本才勉強上線)
第二 并發編程的內存管理都是十分復雜的 而Go語言支持GC即 垃圾回收機制
Go語言為了支持并發編程而內置的上層API是基于 CSP (順序通信進程) 模型 這就意味著顯示鎖都是可以避免的 而Go語言通過相冊安全的通道發送和接受數據以實現同步 這大大簡化了并發程序的編寫
一般情況下 一個普通的桌面計算機系統跑十幾二十個線程就會有點負載了 但是同樣的這臺計算機卻能輕松的讓成百上千甚至過萬個 goroutine進行資源競爭
goroutine
goroutine是什么
goroutine是Go并發設計的核心 說到底 其實它是協程 但是它比線程更小 十幾個goroutine在底層的體現可能是幾個線程
Go語言內部幫你實現了幫你實現了這些goroutine之間的內存共享 執行它只需要極少的棧內存 大概(4~5kb) 正因為如此 可以同時運行成千上萬個goroutine任務
goroutine比thread更高效 更簡單 更輕便
創建goroutine
只需要在函數調用之前添加go關鍵字 就可以創建并發執行單元 開發人員無需了解任何細節 調度器會自動將其安排到合適的系統線程上執行
在并發編程里 我們通常想將一個過程切分成幾塊 并且然后讓每個goroutine負責它的一部分 當一個程序運行時 它的主函數即在一個單獨的goroutine中執行 我們把它叫做 main goroutine
而新的goroutine使用go語句來創建
代碼演示如下
func testnewgor() {for i := 0; i < 5; i++ {fmt.Println("new goroutine say :", i)time.Sleep(time.Second)}
}func main() {go testnewgor()for i := 0; i < 5; i++ {fmt.Println("main goroutine say :", i)time.Sleep(time.Second)}
}
運行這段代碼之后我們會發現主協程 新協程會同時打印語句
如果主goroutine退出
如果說主goroutine推出了 并不會有類似linux中孤兒進程的概念 其他的goroutine也會立即退出
runtime包
Gosched
runtime.gosched() 用于讓出CPU時間片 讓出當前協程的執行權限 調度器會安排其他等待的任務執行 并在下次的某個時刻從該位置開始恢復執行
這就像接力賽一樣 A跑了一段時間遇到代碼runtime.gosched() 之后將接力棒交給B 之后B跑了一段時間遇到代碼runtime.gosched()之后將接力棒交給A
下面是示例代碼
func main() {go func() {for i := 0; i < 5; i++ {runtime.Gosched()fmt.Println("world")}}()// main gorotinuefor i := 0; i < 5; i++ {fmt.Println("hello")runtime.Gosched()}// 最后結果為 hello world hello world ... ...}
Goexit
調用Goexit函數將會立即終止當前goroutine執行 調度器會確保所有的defer調用被執行
下面是示例代碼演示
go func() {defer fmt.Println("this is A")runtime.Goexit()defer fmt.Println("this is B")fmt.Println("this is C")}() // 只會打印 this is A 因為后面的延時調用語句還沒來得及執行協程就退出了 // 不讓主協程退出 觀察其他攜程的掩飾效果for {}
GOMAXPROCS
GOMAXPROCS在Go語言中是一個環境變量 它表示可以Go語言可以并發的最大核心數
如果是 runtime.GOMAXPROCS(size int)
函數 我們有兩種用法
- 第一種是將參數設置0 此時會返回我們當前的最大核心數
- 第二種是將參數設置為其他正整數 此時核心會變為我們設置的值
channel
它和map類似 channel也是一個對于make創建的底層數據結構的引用
當我們復制了一個channel用于函數傳參時 我們只是拷貝了一個channel引用 因此調用者和被調用者將使用同一個channel對象 和其他的引用類型一樣 channel的零值也是nil
定義一個channel時 我們也需要定義發送到chanel值的類型 channel可以使用內置的make()函數來實現
make(chan Type) //等價于make(chan Type, 0)
make(chan Type, capacity)
當capacity等于0的時候 是無緩沖阻塞式讀寫的
當capacity大于0的時候 是有緩沖非阻塞的 直到寫入的數據大于capacity才會阻塞住
channel通過操作符<-來接收和發送數據 發送和接收數據語法如下
channel <- value // 發送value到channel
<- channel // 接受并且丟棄所有數據
x := <- channel // 從channel接受數據 并且賦值給x
x , ok := <- channel // 功能同上 不過增加了一個bool類型的數據來檢查通道是否關閉或者是否為空
在默認情況下 channel接受和發送數據都是阻塞的 除非另一端已經準備好了 這就讓goroutine的同步變得簡單 不需要顯示的lock了
c := make(chan int)go func() {fmt.Println("子協程正在運行")defer fmt.Println("子協程已結束")c <- 666}()fmt.Println("主協程正在運行")x := <-ctime.Sleep(time.Second)fmt.Println("子協程發送的值為", x)
無緩沖的channel
無緩沖的通道(unbuffered channel)是指在接收前沒有能力保存任何值的通道
這種類型的通道要求發送 goroutine 和接收 goroutine 同時準備好,才能完成發送和接收操作。如果兩個goroutine沒有同時準備好,通道會導致先執行發送或接收操作的 goroutine 阻塞等待。
這種對通道進行發送和接收的交互行為本身就是同步的。其中任意一個操作都無法離開另一個操作單獨存在。
我們上面的代碼就是一個無緩沖channel 這里為了方便大家理解再發一遍
c := make(chan int)go func() {fmt.Println("子協程正在運行")defer fmt.Println("子協程已結束")c <- 666}()fmt.Println("主協程正在運行")x := <-ctime.Sleep(time.Second)fmt.Println("子協程發送的值為", x)
有緩沖的channel
有緩沖的channel創建方式如下
make(chan Type, capacity)
此時它阻塞的方式也發生了變化
- 如果緩沖區滿了并且還在寫數據此時會寫入阻塞
- 如果緩沖區空了并且還在讀數據此時會讀取阻塞
range和close
我們可以通過close來關閉一個channel
close (chan)
- channel 不像文件一樣需要經常去關閉 只有當你確實沒有任何發送數據了 或者要結束range循環才關閉
- 關閉之后無法再發送任何的數據 發數據會引發panic異常
- 關閉后可以接受數據
- 接受數據會阻塞住
此外我們還可以通過range迭代來獲取數據 一旦管道關閉 range循環就會結束
單向channel
默認情況下,通道是雙向的,也就是,既可以往里面發送數據也可以同里面接收數據
但是,我們經常見一個通道作為參數進行傳遞而值希望對方是單向使用的,要么只讓它發送數據,要么只讓它接收數據,這時候我們可以指定通道的方向
單向channel變量的聲明非常簡單,如下:
var ch1 chan int // ch1是一個雙向的管道
var ch2 chan<- float64 // ch2只能往里寫入float64數據
var ch3 <-chan int // ch3只能用于接受int類型的數據
- chan<- 表示數據進入管道,要把數據寫進管道,對于調用者就是輸出。
- <-chan 表示數據從管道出來,對于調用者就是得到管道的數據,當然就是輸入
我們可以將channel隱式的轉化為單向隊列只收或者只發 不能將單向的channel轉化為普通channel
轉換的語法如下
c := make(chan int, 3)
var send chan<- int = c // send-only
var recv <-chan int = c // receive-only
下面是完整的使用代碼
func recv(out <-chan int) {for x := range out {fmt.Println(x)}
}func send(in chan<- int) {for i := 0; i < 5; i++ {in <- i * 100}close(in)
}func main() {c := make(chan int, 3)go send(c)recv(c)time.Sleep(3 * time.Second)
}
定時器
Timer
timer是一個定時器 代表未來的一個單一事件 你可以告訴timer這個時間要等待的時間 它會提供一個channel 在將來的那個時間 channel提供了一個時間值
下面是示例代碼
func main() {// 創建定時器 兩秒后定時器就會像自己的c字節發送一個time.TIME類似的元素值timer1 := time.NewTimer(2 * time.Second)t1 := time.Now() // 當前時間fmt.Printf("t1 : %v\n", t1)t2 := <-timer1.Cfmt.Println("t2:", t2)
}
我們在創建定時器之后的兩秒鐘會收到一個時間 之后我們可以將該時間和現在的時間對比一下 我們發現正好相差了兩秒
Ticker
Ticker是一個定時觸發的計時器 它會以一個間隔往channel中發送一個事件 而channel的接收者可以以固定的時間間隔從channel中讀取事件
下面是示例代碼
func main() {// 創建一個定時器 每隔一秒像channel中發送一個事件ticker := time.NewTicker(time.Second * 1)i := 0go func() {for i = 0; i < 5; i++ {<-ticker.Cprintln("goroutine say : ", i)}// 最后關閉tickerticker.Stop()}()for {}
}
Select
Go語言提供了一個關鍵字select 通過select可以監聽channel上的數據流動
select的用法和switch十分相似 由select選擇一個新的模塊 之后每個選擇條件由case語句來描述
此外select語句對比switch語句來說有諸多的限制 其中最大的一條限制就是每一條語句里面必須有一個IO操作 大致結構如下
select {case <-chan1: // 如果chan1成功讀取到數據 則執行該操作// ....case chan2 <- 1: // 如果chan2成被寫入數據 則執行官該操作default:}
在一個select語句中 Go語言會按照順序評估每個發送和接受的語句 如果說有任意條語句可以執行 那么就從這些可執行的語句中任選一條來使用
如果說所有的通道都被阻塞了 那么此時有兩種情況
- 如果給出了default語句 那么就會執行default語句 并且程序會從select語句后恢復
- 如果沒有default語句 那么default語句將會被阻塞 直到一個case可用
func fib(c, q chan int) {x, y := 1, 1for {select {case c <- x: // 如果c輸出了數據x, y = y, x+ycase <-q: // 如果q被寫入了數據fmt.Println("quit")return}}
}func main() {c := make(chan int)quit := make(chan int)go func() {for i := 0; i < 6; i++ {fmt.Println(<-c)}quit <- 0}()fib(c, quit)
}
值得注意的是select中 case c <- x:
的含義 它的意思是 c可以寫入數據的時候執行 那么c什么時候可以寫入數據呢? 當然是有人要接受數據的時候
所以說我們的 fmt.Println(<-c)
語句有兩個作用
- 接受數據并打印
- 讓c可以寫入數據
運行結果如下
超時
有時候我們會遇到goroutine阻塞的情況 那么我們如何避免整個程序陷入阻塞呢 我們可以通過設置超時來實現
語法如下
func main() {c := make(chan int)q := make(chan int)o := make(chan bool)go func() {select {case c <- 0: // 當c可以寫入數據的時候println("可寫入")case <-q: // 當q可以輸出數據的時候println("可輸出")case <-time.After(5 * time.Second):println("超時")o <- falseprintln("我運行完畢了")break}}()<-o
}
這段代碼的最終結果就是打印一個超時之后結束進程