Go并發(fā)4種方法簡明講解
一、goroutine
1、協(xié)程(Coroutine)
Golang 在語言層面對并發(fā)編程進(jìn)行了支持,使用了一種協(xié)程(goroutine)機(jī)制,
協(xié)程本質(zhì)上是一種用戶態(tài)線程,不需要操作系統(tǒng)來進(jìn)行搶占式調(diào)度,但是又寄生于線程中,因此系統(tǒng)開銷極小,可以有效的提高線程的任務(wù)并發(fā)性,而避免多線程的缺點(diǎn)。但是協(xié)程需要語言上的支持,需要用戶自己實(shí)現(xiàn)調(diào)度器,因?yàn)樵贕o語言中,實(shí)現(xiàn)了調(diào)度器所以我們可以很方便的能過 go
關(guān)鍵字來使用協(xié)程。
func main() { for i := 0; i <10; i++ { go func(i int) { for { fmt.Printf("Hello goroutine %d\n",i) } }(i) } time.Sleep(time.Millisecond) }
最簡單的一個并發(fā)編程小例子,并發(fā)輸出一段話。
我們同時開了10個協(xié)程進(jìn)行輸出,每次在fmt.printf
時交出控制權(quán)(不一定每次都會交出控制權(quán)),回到調(diào)度器中,再由調(diào)度器分配。
2、goroutine 可能切換的點(diǎn)
- I/O,Select
- channel
- 等待鎖
- 函數(shù)調(diào)用
- runtime.Gosched()
我們看一個小例子:
func main() { var a [10]int for i := 0; i <10; i++ { go func(i int) { for { a[i]++ } }(i) } time.Sleep(time.Millisecond) fmt.Println(a) }
在這里,代碼直接鎖死,程序沒有退出,因?yàn)樵趫?zhí)行函數(shù)中沒有協(xié)程的切換,因?yàn)?nbsp;main
函數(shù)也是一個協(xié)程。
如果想要程序退出,可以通過 runtime.Gosched()
函數(shù),在執(zhí)行函數(shù)中添加一行。
for { a[i]++ runtime.Gosched() }
加上這個函數(shù)之后,代碼是可以正常執(zhí)行了,但是真的是正常執(zhí)行嗎?不一定,我們可以使用 -reac
命令來看一下數(shù)據(jù)是否有沖突:
這說明數(shù)據(jù)還是有沖突的,數(shù)組a
中的元素一邊在做自增,一邊在輸出。解決這個問題,我們只能使用 channel 來解決。
二、Channel
Channel 中 Go語言在語言級別提供了對 goroutine 之間通信的支持,我們可以使用 channel 在兩個或者多個goroutine之間進(jìn)行信息傳遞,能過 channel 傳遞對像的過程和調(diào)用函數(shù)時的參數(shù)傳遞行為一樣,可以傳遞普通參數(shù)和指針。
Channel 有兩種模式:
var ch1 = make(chan int) // 無緩沖 channel,同步 var ch2 = make(chan int, 2) // 有緩沖 channel, 異步
無緩沖的方式,數(shù)據(jù)進(jìn)入 channel 只要沒有被接收,就會處在阻塞狀態(tài)。
var ch1 = make(chan int) // 無緩沖 channel,同步 ch1 <- 1 ch1 <- 2 // error: all goroutines are asleep - deadlock! fmt.Println(<-ch1)
如果想要運(yùn)行,必須要再開一個協(xié)程不停的去請求數(shù)據(jù):
var ch1 = make(chan int) // 無緩沖 channel,同步 go func() { for { n := <-ch1 fmt.Println(n) } }() ch1 <- 1 ch1 <- 2
有緩沖的方式,只要緩沖區(qū)沒有滿就可以一直進(jìn)數(shù)據(jù),緩沖區(qū)在填滿之后沒有接收也會處理阻塞狀態(tài)。
func bufferChannel() { var ch2 = make(chan int,2) ch2<-1 ch2<-2 fmt.Println(ch2) // 不加這一行的話,是可以正常運(yùn)行的 ch2<-3 // error: all goroutines are asleep - deadlock! }
1、chaanel 指定方向
比如我現(xiàn)在有一個函數(shù)創(chuàng)建一個 channel,并且不斷的需要消費(fèi)channel中的數(shù)據(jù):
func worker(ch chan int) { for { fmt.Printf("hello goroutine worker %d\n", <-ch) } } func createWorker() chan int{ ch := make(chan int) go worker(ch) return ch } func main() { ch := createWorker() ch<-1 ch<-2 ch<-3 time.Sleep(time.Millisecond) }
這個函數(shù)我是要給別人用的,但是我怎么保證使用 createWorker 函數(shù)創(chuàng)建的 channel 都是往里面?zhèn)魅霐?shù)據(jù)的呢?
如果外面有人消費(fèi)了這個 channel 中的數(shù)據(jù),我們怎么限制?
這個時候,我們就可以給返回的channel 加上方向,指明這個 channel 中能往里傳入數(shù)據(jù),不能從中取數(shù)據(jù):
func worker(ch <-chan int) { for { fmt.Printf("hello goroutine worker %d\n", <-ch) } } func createWorker() chan<- int{ ch := make(chan int) go worker(ch) return ch }
我們可以在返回 channel 的地方加上方向,指明返回的函數(shù)只能是一個往里傳入數(shù)據(jù),不能從中取數(shù)據(jù)。
并且我們還可以給專門消費(fèi)的函數(shù)加上一個方向,指明這個函數(shù)只能出不能進(jìn)。
2、channel 關(guān)閉
在使用 channel 的時候,隨說我們可以等待channel中的函數(shù)使用完之后自己結(jié)束,或者等待 main 函數(shù)結(jié)束時關(guān)閉所有的 goroutine 函數(shù),但是這樣的方式顯示不夠優(yōu)雅。
當(dāng)一個數(shù)據(jù)我們明確知道他的結(jié)束時候,我們可以發(fā)送一個關(guān)閉信息給這個 channel ,當(dāng)這個 channel 接收到這個信號之后,自己關(guān)閉。
// 方法一 func worker(ch <-chan int) { for { if c ,ok := <- ch;ok{ fmt.Printf("hello goroutine worker %d\n", c) }else { break } } } // 方法二 func worker(ch <-chan int) { for c := range ch{ fmt.Printf("hello goroutine worker %d\n", c) } } func main() { ch := createWorker() ch<-1 ch<-2 ch<-3 close(ch) time.Sleep(time.Millisecond) }
通過 Close
b函數(shù),我們可以能過 channel 已經(jīng)關(guān)閉,并且我們還可以通過兩種方法判斷通道內(nèi)是否還有值。
三、Select
當(dāng)我們在實(shí)際開發(fā)中,我們一般同時處理兩個或者多個 channel 的數(shù)據(jù),我們想要完成一個那個 channel 先來數(shù)據(jù),我們先來處理個那 channel 怎么辦呢?
此時,我們就可以使用 select 調(diào)度:
func genInt() chan int { ch := make(chan int) go func() { i := 0 for { // 隨機(jī)兩秒以內(nèi)生成一次數(shù)據(jù) time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond) ch <- i i++ } }() return ch } func main() { var c1 = genInt() var c2 = genInt() for { select { case n := <-c1: fmt.Printf("server 1 generator %d\n", n) case n := <- c2: fmt.Printf("server 2 generator %d\n", n) } } }
1、定時器
for { tick := time.Tick(time.Second) select { case n := <-c1: fmt.Printf("server 1 generator %d\n", n) case n := <-c2: fmt.Printf("server 2 generator %d\n", n) case <-tick: fmt.Println("定時每秒輸出一次!") } }
2、超時
for { tick := time.Tick(time.Second) select { case n := <-c1: fmt.Printf("server 1 generator %d\n", n) case n := <-c2: fmt.Printf("server 2 generator %d\n", n) case <-tick: fmt.Println("定時每秒輸出一次!") case <-time.After(1300 * time.Millisecond): // 如果 1.3秒內(nèi)沒有數(shù)據(jù)進(jìn)來,那么就輸出超時 fmt.Println("timeout") } }
四、傳統(tǒng)的并發(fā)控制
1、sync.Mutex
type atomicInt struct { value int lock sync.Mutex } func (a *atomicInt) increment() { a.lock.Lock() defer a.lock.Unlock() // 使用 defer 解鎖,以防忘記 a.value++ } func main() { var a atomicInt a.increment() go func() { a.increment() }() time.Sleep(time.Millisecond) fmt.Println(a.value) }
2、sync.WaitGroup
type waitGrouInt struct { value int wg sync.WaitGroup } func (w *waitGrouInt) addInt() { w.wg.Add(1) w.value++ } func main() { var w waitGrouInt for i := 0; i < 10; i++ { w.addInt() w.wg.Done() } w.wg.Wait() fmt.Println(w.value) }
更多關(guān)于Go并發(fā)簡明講解請查看下面的相關(guān)鏈接
- 使用google-perftools優(yōu)化nginx在高并發(fā)時的性能的教程(完整版)
- Golang極簡入門教程(三):并發(fā)支持
- Go語言并發(fā)技術(shù)詳解
- Go語言并發(fā)模型的2種編程方案
- GO語言并發(fā)編程之互斥鎖、讀寫鎖詳解
- Go語言如何并發(fā)超時處理詳解
- 如何利用Golang寫出高并發(fā)代碼詳解
- golang實(shí)現(xiàn)并發(fā)數(shù)控制的方法
- 詳解Golang 中的并發(fā)限制與超時控制
- golang中sync.Map并發(fā)創(chuàng)建、讀取問題實(shí)戰(zhàn)記錄
- Go 并發(fā)實(shí)現(xiàn)協(xié)程同步的多種解決方法
- 在Go中構(gòu)建并發(fā)TCP服務(wù)器
- Go 并發(fā)控制context實(shí)現(xiàn)原理剖析(小結(jié))
- Go并發(fā)調(diào)用的超時處理的方法
- golang 并發(fā)安全Map以及分段鎖的實(shí)現(xiàn)方法
- golang并發(fā)下載多個文件的方法
- Golang 實(shí)現(xiàn)分片讀取http超大文件流和并發(fā)控制
- golang gin 框架 異步同步 goroutine 并發(fā)操作
相關(guān)文章
Go語言實(shí)現(xiàn)開發(fā)一個簡單的gRPC Demo
這篇文章主要為大家詳細(xì)介紹了如何利用Go語言實(shí)現(xiàn)開發(fā)一個簡單的gRPC Demo,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解一下2023-07-07