每日一抄 Go語言通訊順序程序簡述
阿新 • • 發佈:2022-12-08
package main import ( "fmt" "sync" ) /* Go實現了兩種併發形式,第一種是大家普遍認知的多執行緒共享記憶體,其實就是 Java 或 C++ 等語言中的多執行緒開發;另外一種是Go語言特有的,也是Go語言推薦的 CSP(communicating sequential processes)併發模型。 CSP 併發模型是上個世紀七十年代提出的,用於描述兩個獨立的併發實體通過共享 channel(管道)進行通訊的併發模型。 Go語言就是借用 CSP 併發模型的一些概念為之實現併發的,但是Go語言並沒有完全實現了 CSP 併發模型的所有理論,僅僅是實現了 process 和 channel 這兩個概念。 process 就是Go語言中的 goroutine,每個 goroutine 之間是通過 channel 通訊來實現資料共享。 這裡我們要明確的是“併發不是並行”。併發更關注的是程式的設計層面,併發的程式完全是可以順序執行的,只有在真正的多核 CPU 上才可能真正地同時執行;並行更關注的是程式的執行層面,並行一般是簡單的大量重複,例如 GPU 中對影象處理都會有大量的並行運算。 為了更好地編寫併發程式,從設計之初Go語言就注重如何在程式語言層級上設計一個簡潔安全高效的抽象模型,讓開發人員專注於分解問題和組合方案,而且不用被執行緒管理和訊號互斥這些煩瑣的操作分散精力。 在併發程式設計中,對共享資源的正確訪問需要精確地控制,在目前的絕大多數語言中,都是通過加鎖等執行緒同步方案來解決這一困難問題,而Go語言卻另闢蹊徑,它將共享的值通過通道傳遞(實際上多個獨立執行的執行緒很少主動共享資源)。 併發程式設計的核心概念是同步通訊,但是同步的方式卻有多種。先以大家熟悉的互斥量 sync.Mutex 來實現同步通訊,示例程式碼如下所示: */ /* 由於 mu.Lock() 和 mu.Unlock() 並不在同一個 Goroutine 中,所以也就不滿足順序一致性記憶體模型。同時它們也沒有其他的同步事件可以參考,也就是說這兩件事是可以併發的。 因為可能是併發的事件,所以 main() 函式中的 mu.Unlock() 很有可能先發生,而這個時刻 mu 互斥物件還處於未加鎖的狀態,因而會導致執行時異常。 */ //func main() { // var wg sync.WaitGroup // var mu sync.Mutex // wg.Add(1) // go func() { // defer wg.Done() // fmt.Println("hello") // mu.Lock() // }() // wg.Wait() // //如果main的goroutine不等待就會報錯 // //time.Sleep(time.Second * 5) // mu.Unlock() //} //這樣也能執行 //func main() { // var mu sync.Mutex // // go func() { // fmt.Println("hello") // mu.Unlock() // }() // //} //func main() { // go func() { // fmt.Println("hello") // }() //} /* 執行結果 hello fatal error: sync: unlock of unlocked mutex */ //func main() { // var mu sync.Mutex // mu.Lock() // go func() { // fmt.Println("hello") // mu.Unlock() // }() // mu.Lock() //} /* 修復的方式是在 main() 函式所線上程中執行兩次 mu.Lock(),當第二次加鎖時會因為鎖已經被佔用(不是遞迴鎖)而阻塞,main() 函式的阻塞狀態驅動後臺執行緒繼續向前執行。 當後臺執行緒執行到 mu.Unlock() 時解鎖,此時列印工作已經完成了,解鎖會導致 main() 函式中的第二個 mu.Lock() 阻塞狀態取消,此時後臺執行緒和主執行緒再沒有其他的同步事件參考,它們退出的事件將是併發的,在 main() 函式退出導致程式退出時,後臺執行緒可能已經退出了,也可能沒有退出。雖然無法確定兩個執行緒退出的時間,但是列印工作是可以正確完成的。 */ //使用 sync.Mutex 互斥鎖同步是比較低階的做法,我們現在改用無快取通道來實現同步: //func main() { // done := make(chan int) // go func() { // fmt.Println("hello") // h := <-done // fmt.Println("hhhhhhhhhhhhhh", h, '\n') // }() // done <- 1 // done <- 2 //} /* 根據Go語言記憶體模型規範,對於從無快取通道進行的接收,發生在對該通道進行的傳送完成之前。因此,後臺執行緒<-done 接收操作完成之後,main 執行緒的done <- 1 傳送操作才可能完成(從而退出 main、退出程式),而此時列印工作已經完成了。 上面的程式碼雖然可以同步,但是對通道的快取大小太敏感,如果通道有快取,就無法保證main()函式退出之前後臺執行緒 就能正常列印了,更好的做法是將通道的傳送和接收方向調換一下,這樣可以避免同步事件受通道快取大小的影響 */ //func main() { // done := make(chan int,1) // go func() { // fmt.Println("hello") // done <- 1 // done <- 2 // }() // <-done //} /* 對於帶快取的通道,對通道的第K個接收完成操作發生在第K+C個傳送之前,其中C是通道的的快取大小。雖然通道是帶快取的。 但是main執行緒接收完成是在後臺執行緒傳送開始但是還未完成的時刻此時列印工作也是已經完成的 */ //基於帶快取通道,我們可以很容易將列印執行緒擴充套件到N個,下面的示例時開啟十個後臺執行緒分別列印 //func main() { // done := make(chan int, 10) //帶10個快取 // /*開啟N個後臺列印執行緒*/ // for i := 0; i < cap(done); i++ { // go func() { // fmt.Println("hello") // done <- 1 // }() // } // // //等待N個後臺執行緒完成 // for i := 0; i < cap(done); i++ { // <-done // } //} //對於這種要等待N個執行緒完成後再進行下一步的同步操作有一個簡單的方法,就是使用sync.WaitGroup來等待一組事件 func main() { var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() fmt.Println("hello") }() } wg.Wait() } /* 其中 wg.Add(1) 用於增加等待事件的個數, 必須確保在後臺執行緒啟動之前執行(如果放到後臺執行緒之中執行則不能保證被正常執行到)。 當後臺執行緒完成列印工作之後, 呼叫 wg.Done() 表示完成一個事件,main() 函式的 wg.Wait() 是等待全部的事件完成。 */