Go語言中的并發指的是能讓某個函數獨立于其他函數運行的能力。當一個函數創建為goroutine時,Go會將其視為一個獨立的工作單元。
操作系統會在物理處理器上調度線程來運行,而Go語言運行時會在邏輯處理器上調度 goroutine 來運行。每個邏輯處理器都分別綁定到單個操作系統線程。如果創建一個 goroutine 并運行,那么這個 goroutine 就會被放到調度器的全局運行隊列中,之后調度器就會將這些隊列中的 goroutine 分配給一個邏輯處理器,并將其放到這個邏輯處理器對應的本地運行隊列中。本地運行隊列中的 goroutine 會一直等待直到自己被分配的邏輯處理器執行。
goroutine 協程
我們通過一個在邏輯處理器上運行的例子來理解調度器的行為與如何管理 goroutine。
func main() {
//分配一個邏輯處理器給調度器使用
runtime.GOMAXPROCS(1)
//wg 用來等待線程完成
var wg sync.WaitGroup
//Add(2)表示要等待兩個goroutine
wg.Add(2)
fmt.Println("start goroutines")
//聲明匿名函數,并創建一個goroutine
go func() {
//在函數退出時調用Done來通知main函數工作已經完成
defer wg.Done()
for i := 0;i<10;i++{
fmt.Println("func1: ",i)
}
}()
//聲明匿名函數,并創建一個goroutine
go func() {
//在函數退出時調用Done來通知main函數工作已經完成
defer wg.Done()
for i := 20;i<30;i++{
fmt.Println("func2: ",i)
}
}()
fmt.Println("Wating to finish")
//等待所以 goroutine 結束
//這里如果不設置就有可能會導致main函數在運行兩個goroutine完成之前提前退出,這樣程序就有可能提前終止
wg.Wait()
}
函數說明:
runtime.GOMAXPROCS(num):允許程序更改調度器可以使用的邏輯處理器的數量,如: runtime.GOMAXPROCS(runtime.NumCPU()),為每個可用的物理處理器創建一個邏輯處理器。
sync.WaitGroup:是一個計數信號量,可以用來記錄并維護運行 goroutine 。如果WaitGroup的值大于0,就會導致 wg.Wait() 被阻塞,每執行一次wg.Done(),WaitGroup的值就會減一,最終為0.
基于調度器的內部算法,一個正在運行的 goroutine 可能會被停止并重新調度,這樣的目的是為了防止某個goroutine 長時間占用邏輯處理器。如果多個goroutine 在沒有互相同步的情況下訪問某個共享的資源,那么就可能產生競態。
競態檢測器
go build -race //競態檢測器
./example //運行程序
來看一下競態的小示例
var(
counter int
wg sync.WaitGroup
)
func main() {
wg.Add(2)
go IncCounter(1)
go IncCounter(2)
wg.Wait()
fmt.Println(counter)
}
func IncCounter(id int) {
defer wg.Done()
for count:=0;count<2;count++{
value := counter
//讓出對處理器的占用
runtime.Gosched()
value ++
counter = value
}
}
輸出結果并不一定等于4,這是因為每個 goroutine 都會覆蓋掉原來 goroutine 的工作內容,也就是說當第一個goroutine 進入到IncCounter時,運行到 runtime.Gosched()時,會讓出對處理器的占用,然后讓第二個 goroutine 進入到 IncCounter執行,這樣就會導致原來的 counter 被覆蓋掉,最終結果就會出現錯誤。
同步工具
①原子函數:從底層的加鎖機制來同步訪問整型變量和指針
將上面的函數改為原子函數操作
func IncCounter(id int) {
defer wg.Done()
for count:=0;count<2;count++{
atomic.AddInt64(&counter,1)
//放棄對處理器的占用,回到隊列中,相當于java中的yeild
runtime.Gosched()
}
}
②互斥鎖
互斥鎖用于在代碼上創建一個臨界區,保證同一時間只有一個 goroutine 可以執行這個臨界區代碼。
func IncCounter(id int) {
defer wg.Done()
for count := 0; count < 2; count++ {
mutex.Lock()
{
value := counter
runtime.Gosched()
value ++
counter = value
}
mutex.Unlock()
}
}
③通道
除了上面兩個方式進行同步消除競態以外,還可以使用通道來解決競爭問題。
當一個資源需要在 goroutine 中被共享時,通道會在goroutine之間建立一個管道,并提供同步交換數據的機制。聲明通道時,需要指定被共享的數據類型。通道分為無緩沖的通道與有緩沖的通道。向通道發送數據需要用到<-操作符
court := make(chan int) //無緩沖通道
court := make(chan int,10)//有緩沖通道
court<- 1 //向通道發送整數 1
value,ok := <-court //從通道中接收數據,value:接收的通道數據,ok:通道是否被關閉,true表示正常開啟
無緩沖的通道是指接收前沒有能力保存任何值的通道,這種類型的通道要求發送 goroutine 與接收 goroutine 要同時準備好,如果沒有同時準備好,那么先發送或者先接收的 goroutine 就會進行阻塞等待。
//無緩沖通道示例
var wg1 sync.WaitGroup
//init初始化包,Go語言運行時會在其他代碼執行前優先執行這個
func init() {
//設置隨機數種子
rand.Seed(time.Now().UnixNano())
}
func main(){
court := make(chan int)
wg1.Add(2)
go player("A",court)
go player("B",court)
//開始發球
court<- 1
}
func player(name string,court chan int) {
defer wg1.Done()
for {
ball,ok := <-court //ok:表示接收到的值有效
if !ok{
//說明通道被關閉
fmt.Printf("name: %v is win\n",name)
return
}
n := rand.Intn(100)
if n%13 == 0{
fmt.Printf("Player %v is missed\n",name)
//通道已被關閉
close(court)
return
}
fmt.Printf("Player %v Hit %v\n",name,ball)
ball++
//將球打回對方
court <- ball
}
}
有緩沖的通道是指在通道數據被接收前有能力保存一個或多個的值,這種類型的通道并不要求發送 goroutine 與接收 goroutine 要同時準備好。只有在通道中沒有多余的緩存空間保存值的時候,發送 goroutine 的動作才會進行阻塞,同理只有在通道中沒有要接收的值時,接收動作才會被阻塞。如果緩沖區已滿,再往通道發送數據時就會報錯
func main() {
ch := make(chan int, 1)
ch <- 1
ch <- 2
fmt.Println(<-ch)
fmt.Println(<-ch)
}
----output----
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
D:/GoDemo/src/MyGo/Demo_03.go:8 +0x7a
與無緩沖通道相比不同點:無緩沖通道能保證通道中發送與接收動作是同時進行的,而有緩沖的通道則不會保證。
通道關閉:
在有緩存的通道執行通道關閉( close(court))后,goroutine依舊可以從通道中接收數據,這樣有利于將緩存中的所有數據都接收,從而不會使得數據丟失,但是并不能往通道中發送數據。只有發送者才能關閉信道,而接收者不能。向一個已經關閉的信道發送數據會引發程序恐慌(panic)。信道與文件不同,通常情況下無需關閉它們。只有在必須告訴接收者不再有值需要發送的時候才有必要關閉,例如終止一個 range 循環。