golang 之并发
在了解之前,要注意golang是并發語言而不是并行語言
并發和并行
并發是一次性做大量事情的能力(兩個或多個事件在同一時間間隔發生)
并行同一時間執行多個任務的能力(兩個或者多個事件在同一時刻發生)
舉例說明:
每天早上10分鐘我洗臉,刷牙,吃早飯等等很多事情,這就是并發。 我一邊刷牙的同時在燒水做飯這就是并行。
技術層面來說:假如一個web網頁中有視頻播放和文件下載兩個動作,當瀏覽器在單核的處理器下運行時,CPU核心會在這兩個事件中來回切換,(同時)播放視頻和下載,這就稱為并發。并發進程在不同的時間點開始并有著重疊的執行周期。假如你的CPU是多核處理器,那么下載和播放會在不同的CPU核心同時執行,這就是并行。
goroutine
在go中,每一個并發執行的操作都稱為goroutine,當一個程序啟動時,只有一個goroutine來調用main函數,稱它為主goroutine。新的goroutine通過go語法來創建。
f() // 調用f(); 等待它返回 go f() //新建一個調用f()的goroutine,不用等待。
調度模型
groutine能擁有強大的并發實現是通過GPM調度模型實現:
G:代表一個goroutine,它有自己的棧,instruction pointer和其他信息(正在等待的channel等等),用于調度。
M: 代表內核級線程,一個M就是一個線程,goroutine就是跑在M之上的;M是一個很大的結構,里面維護小對象內存cache(mcache)、當前執行的goroutine、隨機數發生器等等非常多的信息
P: 全程processor,處理器,它的主要作用來執行goroutine,所以它維護了一個goroutine隊列。里面存儲了所有需要它來執行的goroutine。
Sched:代表調度器,它維護有存儲M和G的隊列以及調度器的一些狀態信息等。
調度實現
有2個物理線程M,每一個M都擁有一個處理器P,每一個也都有一個正在運行的goroutine。
P的數量可以通過GOMAXPROCS()來設置,它其實也就代表了真正的并發度,即有多少個goroutine可以同時運行。圖中灰色的那些goroutine并沒有運行,而是出于ready的就緒態,正在等待被調度。P維護著這個隊列(稱之為runqueue),
Go語言里,啟動一個goroutine很容易:go function 就行,所以每有一個go語句被執行,runqueue隊列就在其末尾加入一個goroutine,在下一個調度點,就從runqueue中取出(如何決定取哪個goroutine?)一個goroutine執行。
如果一個線程阻塞會發生什么情況呢?如下圖
從上圖中可以看出,一個線程放棄了它的上下文讓其他的線程可以運行它。M1可能僅僅為了讓它處理圖中系統調用而被創建出來,或者它可能來自一個線程池。這個處于系統調用中的線程將會保持在這個導致系統調用的goroutine上,因為從技術上來說,它仍然在執行,雖然阻塞在OS里了。
另一種情況是P所分配的任務G很快就執行完了(分配不均),這就導致了這個處理器P很忙,但是其他的P還有任務,此時如果global runqueue沒有任務G了,那么P不得不從其他的P里拿一些G來執行。一般來說,如果P從其他的P那里要拿任務的話,一般就拿run queue的一半,這就確保了每個OS線程都能充分的使用,
使用goroutine
package main
import (
"fmt"
"time"
)
func cal(a int , b int ) {
c := a+b
fmt.Printf("%d + %d = %d
",a,b,c)
}
func main() {
for i :=0 ; i<10 ;i++{
go cal(i,i+1) //啟動10個goroutine 來計算
}
time.Sleep(time.Second * 2) // sleep作用是為了等待所有任務完成
}
GOMAXPROCS
設置goroutine運行的CPU數量,最新版本的go已經默認已經設置了。
num := runtime.NumCPU() //獲取主機的邏輯CPU個數 runtime.GOMAXPROCS(num) //設置可同時執行的最大CPU數
也可以根據個人手動設置,例如
func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
}
func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
}
func main() {
runtime.GOMAXPROCS(1)
go a()
go b()
time.Sleep(time.Second)
上面GOMAXPROCS設置為1,當遇到兩個go調度時,就會發生等待。如果設置為2就會并行執行(前提是你的cpu數量>=2),如下例子
func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
}
func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
}
func main() {
runtime.GOMAXPROCS(2)
go a()
go b()
time.Sleep(time.Second)
}
在執行上面代碼時細心的同學會發現,每一步最后都會睡眠一秒,才能打印結果,這是因為并發執行,goroutine還沒來得及返回結果,主線程已經執行完了。
那么如果不會面有沒有其他的方法?當然有。第一種便是采用sync.WaitGroup來實現
var wg sync.WaitGroup
func hello(i int) {
defer wg.Done() // goroutine結束就登記-1
fmt.Println("Hello Goroutine!", i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // 啟動一個goroutine就登記+1
go hello(i)
}
wg.Wait() // 等待所有登記的goroutine都結束
}
詳細用法詳見sync包。另外一種便是channel
channel
channel是用來傳遞數據的一個數據結構,同map一樣使用內置的make來創建。如
ch := make(chan int) // 無緩沖通道 ch1 := make(chan int, 10) //緩沖為10的通道
channel類型
定義格式
ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .
它表示三種類型的定義,可選的<-表示channel的方向。如果沒有指定,即表示雙向通道,既可以接收,也可以發送。
chan T // 可以接收和發送類型為 T 的數據 chan<- float64 // 只可以用來發送 float64 類型的數據 <-chan int // 只可以用來接收 int 類型的數據
<-總是最優先與最左邊類型結合。如
chan<- chan int // 等價 chan<- (chan int) chan<- <-chan int // 等價 chan<- (<-chan int) <-chan <-chan int // 等價 <-chan (<-chan int) chan (<-chan int)
channel操作
常見三種操作,接收,發送和關閉
ch := make(chan int)
發送:ch <- 1 //將1發送到ch通道中
接收:x := <-ch // 從ch接收值并賦給x。也可以直接拋棄:<-ch
關閉:close(ch)
close時可以通過i, ok := <-c可以查看Channel的狀態,判斷值是零值還是正常讀取的值。
c := make(chan int, 10)
close(c)
i, ok := <-c
fmt.Printf("%d, %t", i, ok) //0, false
無緩沖通道
需要注意的是,無緩沖通道上的發送操作將會阻塞,值到另一個goroutine在對立的通道上執行接收操作。這時值才算完成。相反,如果接收操作先執行,接收方goroutine將阻塞,直到另一個goroutine在同一個通道發送一個值。
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 把 sum 發送到通道 c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 從通道 c 中接收
fmt.Println(x, y, x+y)
}
打印結果
-5 17 12
單向通道
當程序演進時,將大的函數拆分為多個更小的是很自然的,在當一個通道用作函數的行參時,它幾乎總是被有意地限制不能發送或接收。為了將這種意圖可以比避免誤用,在go的類型系統提供了單向通道。僅僅導出發送或者接收操作。如類型chan <- int是一個只能發送的通道。反之 <- chan int是一個只能接收int類型通道。
func sum(out chan<- int) {
for i := 0; i < 100; i++ {
out <- i
}
close(out)
}
func squarer(out chan<- int, in <-chan int) {
for i := range in {
out <- i * i
}
close(out)
}
func printer(in <-chan int) {
for i := range in {
fmt.Println(i)
}
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go sum(ch1)
go squarer(ch2, ch1)
}
有緩沖通道
緩沖通道有一個元素隊列,隊列的最大長度在創建時通過make的容量參數來設置。
ch := make(chan string, 3)
一個空的緩沖通道
緩沖通道上的發送操作在隊列的尾部插入一個元素,接收操作從隊列的頭部移除一個元素。如果通道滿了,發送操作將會阻塞所在的goroutine直到另一個goroutine對它進行移除操作留出可用的空間。反過來,如果通道空了。執行接收操作的goroutine阻塞,直到另一個goroutine在通道上發送數據。
func main() {
ch := make(chan string, 3)
ch <- "a"
ch <- "b"
ch <- "c"
fmt.Println("發送成功")
x := <-ch // 打印a
}
range
func main() {
go func() {
time.Sleep(1 * time.Hour)
}()
c := make(chan int)
go func() {
for i := 0; i < 10; i = i + 1 {
c <- i
}
close(c)
}()
for i := range c {
fmt.Println(i)
}
fmt.Println("Finished")
}
如上面的例子,range c 產生的迭代值為channel中發送的值,它會一直迭代直到channel關閉。如果此時close(c)關掉。程序會一直阻塞在for....range c 這一行。
select
select語句選擇一組可能的send操作和receive操作去處理。它類似switch,但是只是用來處理通訊(communication)操作。它的case可以是send語句,也可以是receive語句,亦或者default。receive語句可以將值賦值給一個或者兩個變量。它必須是一個receive操作。最多允許有一個default case,它可以放在case列表的任何位置,盡管我們大部分會將它放在最后。
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
如果有同時多個case去處理,比如同時有多個channel可以接收數據,那么Go會偽隨機的選擇一個case處理(pseudo-random)。如果沒有case需要處理,則會選擇default去處理,如果default case存在的情況下。如果沒有default case,則select語句會阻塞,直到某個case需要處理。
特別注意的是nil channel會一直被阻塞。如果沒有default: nil chanel會一直阻塞。
最后列出channel的幾種常用關系
小結:
在處理并發時會發生數據錯亂的情況,這時候就會用到鎖機制,如上面一開始介紹sync包。鎖將會在sync包中描述。
總結
以上是生活随笔為你收集整理的golang 之并发的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何识别电脑硬件型号如何查看自己的电脑型
- 下一篇: 路由器怎么设定指定IP路由器如何指定网段