golang:協程安全
多路複用
Go語言中提供了一個關鍵字select
,通過select
可以監聽channel
上的資料流動。select的用法與switch語法類似,由select開始一個新的選擇塊,每個選擇條件由case語句來描述。只不過,select的case
有比較多的限制,其中最大的一條限制就是每個case語句裡必須是一個IO操作。
select 語法如下:
select { case <-chan1: // 如果chan1成功讀到資料,則進行該case處理語句 case chan2 <- 1: // 如果成功向chan2寫入資料,則進行該case處理語句 default: // 如果上面都沒有成功,則進入default處理流程 }
在一個select語句中,會按順序從頭至尾評估每一個傳送和接收的語句;如果其中的任意一語句可以繼續執行(即沒有被阻塞),那麼就從那些可以執行的語句中任意選擇一條來使用。如果沒有任意一條語句可以執行(即所有的通道都被阻塞),那麼有兩種可能的情況:⑴ 如果給出了default語句,那麼就會執行default語句,同時程式的執行會從select語句後的語句中恢復。⑵ 如果沒有default語句,那麼select語句將被阻塞,直到至少有一個channel可以進行下去。
在一般的業務場景下,select不會用default
,當監聽的流中再沒有資料,IO操作就 會阻塞現象,如果使用了default
,此時可以出讓CPU時間片。如果使用了default
阻塞與非阻塞使用場景
- 阻塞: 如:在監聽超時退出時,如果100秒內無操作,擇退出,此時添加了default會形成忙輪訓,超時監聽變成了無效。
- 非阻塞: 如,在一個只有一個業務邏輯處理時,主程序控制程序的退出。此時可以使用default。
定時器
Go語言中定時器的使用有三個方法
time.Sleep()
time.NewTimer()
返回一個時間的管道, time.C 讀取管道的內容time.After(5 * time.Second)
封裝了time.NewTimer(),反回了一個time.C
的管道
示例
select { case <-time.After(time.Second * 10): }
鎖和條件變數
Go語言中為了解決協程間同步問題,提供了標準庫程式碼,包sync
和sync/atomic
中。
互斥鎖
互斥鎖是傳統併發程式設計對共享資源進行訪問控制的主要手段,它由標準庫sync中的Mutex結構體型別表示。sync.Mutex型別只有兩個公開的指標方法,Lock和Unlock。Lock鎖定當前的共享資源,Unlock進行解鎖。
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
var mutex sync.Mutex
func print(str string) {
mutex.Lock() // 新增互斥鎖
defer mutex.Unlock() // 使用結束時解鎖
for _, data := range str { // 迭代器
fmt.Printf("%c", data)
time.Sleep(time.Second) // 放大協程競爭效果
}
fmt.Println()
}
func main() {
go print("hello") // main 中傳參
go print("world")
for {
runtime.GC()
}
}
讀寫鎖
讀寫鎖的使用場景一般為讀多寫少,可以讓多個讀操作併發,同時讀取,但是對於寫操作是完全互斥的。也就是說,當一個goroutine進行寫操作的時候,其他goroutine不能進行讀寫操作;當一個goroutine獲取讀鎖之後,其他的goroutine獲取寫鎖都會等待
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var count int // 全域性變數count
var rwlock sync.RWMutex // 全域性讀寫鎖 rwlock
func read(n int) {
for {
rwlock.RLock()
fmt.Printf("reading goroutine %d ...\n", n)
num := count
fmt.Printf("read goroutine %d finished,get number %d\n", n, num)
rwlock.RUnlock()
}
}
func write(n int) {
for {
rwlock.Lock()
fmt.Printf("writing goroutine %d ...\n", n)
num := rand.Intn(1000)
count = num
fmt.Printf("write goroutine %d finished,write number %d\n", n, num)
rwlock.Unlock()
}
}
func main() {
for i := 0; i < 5; i++ {
go read(i + 1)
time.Sleep(time.Microsecond * 100)
}
for i := 0; i < 5; i++ {
go write(i + 1)
time.Sleep(time.Microsecond * 100)
}
for {
}
}
可以看出,讀寫鎖控制下的多個寫操作之間都是互斥的,並且寫操作與讀操作之間也都是互斥的。但是,多個讀操作之間不存在互斥關係。
Go語言中的死鎖
死鎖 deadlock
是指兩個或兩個以上的程序在執行過程中,由於競爭資源或者由於彼此通訊而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖。
單gorutine同時讀寫,寫死鎖
在一個gorutine中,當channel無緩衝,寫阻塞,等待讀取導致死鎖
解決,應該至少在2個gorutine進行channle通訊,或者使用緩衝區。
package main
func main() {
channel := make(chan int)
channel <- 1
<-channel
}
多gorutine使用一個channel通訊,寫先於讀
程式碼順序執行時,寫操作阻塞,導致後面協程無法啟動進行讀操作,導致死鎖
package main
func main() {
channel := make(chan int)
channel <- 1
go func() {
<-channel
}()
}
多channel交叉死鎖
在goroutine中,多個goroutine使用多個channel互相等待對方寫入,導致死鎖
package main
func main() {
channel1 := make(chan int)
channel2 := make(chan int)
go func() {
select {
case <-channel1:
channel2 <- 1
}
}()
select {
case <-channel2:
channel1 <- 1
}
}