Go并發(fā)4種方法簡明講解
一、goroutine
1、協(xié)程(Coroutine)
Golang 在語言層面對并發(fā)編程進行了支持,使用了一種協(xié)程(goroutine)機制,
協(xié)程本質(zhì)上是一種用戶態(tài)線程,不需要操作系統(tǒng)來進行搶占式調(diào)度,但是又寄生于線程中,因此系統(tǒng)開銷極小,可以有效的提高線程的任務并發(fā)性,而避免多線程的缺點。但是協(xié)程需要語言上的支持,需要用戶自己實現(xiàn)調(diào)度器,因為在Go語言中,實現(xiàn)了調(diào)度器所以我們可以很方便的能過 go關鍵字來使用協(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é)程進行輸出,每次在fmt.printf時交出控制權(不一定每次都會交出控制權),回到調(diào)度器中,再由調(diào)度器分配。
2、goroutine 可能切換的點
- 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)
}
在這里,代碼直接鎖死,程序沒有退出,因為在執(zhí)行函數(shù)中沒有協(xié)程的切換,因為 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之間進行信息傳遞,能過 channel 傳遞對像的過程和調(diào)用函數(shù)時的參數(shù)傳遞行為一樣,可以傳遞普通參數(shù)和指針。
Channel 有兩種模式:
var ch1 = make(chan int) // 無緩沖 channel,同步 var ch2 = make(chan int, 2) // 有緩沖 channel, 異步
無緩沖的方式,數(shù)據(jù)進入 channel 只要沒有被接收,就會處在阻塞狀態(tài)。
var ch1 = make(chan int) // 無緩沖 channel,同步 ch1 <- 1 ch1 <- 2 // error: all goroutines are asleep - deadlock! fmt.Println(<-ch1)
如果想要運行,必須要再開一個協(xié)程不停的去請求數(shù)據(jù):
var ch1 = make(chan int) // 無緩沖 channel,同步
go func() {
for {
n := <-ch1
fmt.Println(n)
}
}()
ch1 <- 1
ch1 <- 2
有緩沖的方式,只要緩沖區(qū)沒有滿就可以一直進數(shù)據(jù),緩沖區(qū)在填滿之后沒有接收也會處理阻塞狀態(tài)。
func bufferChannel() {
var ch2 = make(chan int,2)
ch2<-1
ch2<-2
fmt.Println(ch2)
// 不加這一行的話,是可以正常運行的
ch2<-3 // error: all goroutines are asleep - deadlock!
}
1、chaanel 指定方向
比如我現(xiàn)在有一個函數(shù)創(chuàng)建一個 channel,并且不斷的需要消費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ù)的呢?
如果外面有人消費了這個 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ù)。
并且我們還可以給專門消費的函數(shù)加上一個方向,指明這個函數(shù)只能出不能進。
2、channel 關閉
在使用 channel 的時候,隨說我們可以等待channel中的函數(shù)使用完之后自己結(jié)束,或者等待 main 函數(shù)結(jié)束時關閉所有的 goroutine 函數(shù),但是這樣的方式顯示不夠優(yōu)雅。
當一個數(shù)據(jù)我們明確知道他的結(jié)束時候,我們可以發(fā)送一個關閉信息給這個 channel ,當這個 channel 接收到這個信號之后,自己關閉。
// 方法一
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)
}
通過 Closeb函數(shù),我們可以能過 channel 已經(jīng)關閉,并且我們還可以通過兩種方法判斷通道內(nèi)是否還有值。
三、Select
當我們在實際開發(fā)中,我們一般同時處理兩個或者多個 channel 的數(shù)據(jù),我們想要完成一個那個 channel 先來數(shù)據(jù),我們先來處理個那 channel 怎么辦呢?
此時,我們就可以使用 select 調(diào)度:
func genInt() chan int {
ch := make(chan int)
go func() {
i := 0
for {
// 隨機兩秒以內(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ù)進來,那么就輸出超時
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)
}
更多關于Go并發(fā)簡明講解請查看下面的相關鏈接
- 使用google-perftools優(yōu)化nginx在高并發(fā)時的性能的教程(完整版)
- Golang極簡入門教程(三):并發(fā)支持
- Go語言并發(fā)技術詳解
- Go語言并發(fā)模型的2種編程方案
- GO語言并發(fā)編程之互斥鎖、讀寫鎖詳解
- Go語言如何并發(fā)超時處理詳解
- 如何利用Golang寫出高并發(fā)代碼詳解
- golang實現(xiàn)并發(fā)數(shù)控制的方法
- 詳解Golang 中的并發(fā)限制與超時控制
- golang中sync.Map并發(fā)創(chuàng)建、讀取問題實戰(zhàn)記錄
- Go 并發(fā)實現(xiàn)協(xié)程同步的多種解決方法
- 在Go中構(gòu)建并發(fā)TCP服務器
- Go 并發(fā)控制context實現(xiàn)原理剖析(小結(jié))
- Go并發(fā)調(diào)用的超時處理的方法
- golang 并發(fā)安全Map以及分段鎖的實現(xiàn)方法
- golang并發(fā)下載多個文件的方法
- Golang 實現(xiàn)分片讀取http超大文件流和并發(fā)控制
- golang gin 框架 異步同步 goroutine 并發(fā)操作
相關文章
Go語言實現(xiàn)開發(fā)一個簡單的gRPC Demo
這篇文章主要為大家詳細介紹了如何利用Go語言實現(xiàn)開發(fā)一個簡單的gRPC Demo,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起了解一下2023-07-07


