1. 程式人生 > 實用技巧 >golang:協程安全

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

就形成了非阻塞狀態,形成了忙輪訓,會佔用CPU、系統資源。

阻塞與非阻塞使用場景

  • 阻塞: 如:在監聽超時退出時,如果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語言中為了解決協程間同步問題,提供了標準庫程式碼,包syncsync/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
	}
}

隱性死鎖