golang 之sync &並發安全鎖


並發安全(競態問題)

  讓一個程序並發安全並不需要其中的每一個具體類型都是並發安全的。實際上並發安全的類型其實是特例而不是普遍存在的,所以僅在文檔指出類型是安全的情況下,才可以並發的訪問一個變量。與之對應的是,導出的包級別函數通常可以認為是並發安全的。因為包級別的變量無法限制在一個goroutine內。所以那些修改這些變量的函數必須采用互斥機制。

  例如下面代碼就會存在競態問題導致結果與與其不否

var x int64
var wg sync.WaitGroup

func add() {
	for i := 0; i < 5000; i++ {
		x = x + 1
	}
	wg.Done()
}
func main() {
	wg.Add(2)
	go add()
	go add()
	wg.Wait()
	fmt.Println(x)
}

  這時引入鎖至關重要,在go中sync包提供了鎖機制

sync包  

  Sync包同步提供基本的同步原語,如互斥鎖。 除了Once和WaitGroup類型之外,大多數類型都是供低級庫例程使用的。 通過Channel和溝通可以更好地完成更高級別的同步。並且此包中的值在使用過后不要拷貝。

  sync包中主要有:Locker, Cond, Map, Mutex, Once, Pool,、RWMutex, WaitGroup

互斥鎖:sync.Mutex

  互斥鎖的模式應用非常廣泛,所以sync包有一個單獨的Mutex類型來支持這種模式,它的Lock方法用來獲取令牌(token,此過程也稱為上鎖), Unlock方法用於釋放令牌。

var x int64
var wg sync.WaitGroup
var lock sync.Mutex

func add() {
	for i := 0; i < 5000; i++ {
		lock.Lock() // 加鎖
		x = x + 1
		lock.Unlock() // 解鎖
	}
	wg.Done()
}
func main() {
	wg.Add(2)
	go add()
	go add()
	wg.Wait()
	fmt.Println(x)
}

 在Lock和Unlock之間的代碼,可以自由的讀取和修改共享變量,這一部分稱為臨界區域。在鎖的持有人調用Unlock之前,其他的goroutine不能獲取鎖。

讀寫互斥鎖:sync.RWMutex

  在某種情況下,函數只須讀取變量的狀態,所以多個函數可以安全的並發執行。只要在寫入沒有同時就行,在這種場景下,就需要一種特殊的安全鎖,它只允許讀操作可以並發執行,但寫操作需要獲得安全獨享的訪問權限。

var (
	x      int64
	wg     sync.WaitGroup
	lock   sync.Mutex
	rwlock sync.RWMutex
)

func write() {
	// lock.Lock()   // 加互斥鎖
	rwlock.Lock() // 加寫鎖
	x = x + 1
	time.Sleep(10 * time.Millisecond) // 假設讀操作耗時10毫秒
	rwlock.Unlock()                   // 解寫鎖
	// lock.Unlock()                     // 解互斥鎖
	wg.Done()
}

func read() {
	// lock.Lock()                  // 加互斥鎖
	rwlock.RLock()               // 加讀鎖
	time.Sleep(time.Millisecond) // 假設讀操作耗時1毫秒
	rwlock.RUnlock()             // 解讀鎖
	// lock.Unlock()                // 解互斥鎖
	wg.Done()
}

func main() {
	start := time.Now()
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go write()
	}

	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go read()
	}

	wg.Wait()
	end := time.Now()
	fmt.Println(end.Sub(start))
}

延遲初始化:sync.Once

  延遲是一個昂貴的初始化步驟到有實際需求的時刻是一個很好的實踐。而sync.Once是一個可以被多次調用但是只執行一次,若每次調用Do時傳入參數f不同,但是只有第一個才會被執行。

sync.Once有一個 Do方法。示例如下

var once sync.Once
    onceBody := func() {
        fmt.Println("Only once")
    }
    done := make(chan bool)
    for i := 0; i < 10; i++ {
        go func() {
            once.Do(onceBody)
            done <- true
        }()
    }
    for i := 0; i < 10; i++ {
        <-done
    }

 執行雖然調用了10次,但是只執行了1次。BTW:這個東西可以用來寫單例。

單例(借助Once)

package singleton

import (
    "sync"
)

type singleton struct {}

var instance *singleton
var once sync.Once

func GetInstance() *singleton {
    once.Do(func() {
        instance = &singleton{}
    })
    return instance
}

sync.WaitGroup

  waitgroup 用來等待一組goroutines的結束,在主Goroutine里聲明,並且設置要等待的goroutine的個數,每個goroutine執行完成之后調用 Done,最后在主Goroutines 里Wait即可。waitgroup含有三種方法

func (wg *WaitGroup) Add(d int)  //計數器+d
func (wg *WaitGroup) Done()  //計數器-1
func (wg *WaitGroup) Wait()  //阻塞直到計數器變為0

 一個簡單的例子

var wg sync.WaitGroup

func hello() {
	defer wg.Done()
	fmt.Println("Hello Goroutine!")
}
func main() {
	wg.Add(1)
	go hello() // 啟動另外一個goroutine去執行hello函數
	fmt.Println("main goroutine done!")
	wg.Wait()
}

競態檢測器(race detector) 

  在編寫時即使最大的仔細還會出現並發上的錯誤,幸運的是,go語言在運行時和工具鏈裝備了一個精致並易於使用的動態分析工具:競態檢測器。

簡單的把 -race命令行參數加到go build, go run, go test命令里邊即可使用該功能。它會讓編譯器為你的應用或測試構建一個修訂后的版本。

  競態檢測器會研究事件流,找到那些有問題的案例,即一個goroutine寫入一個變量后,中間沒有任何同步的操作,就有另一個goroutine寫入了該變量。這種案例表明有對共享變量的並發訪問,即數據動態。

  競態檢測器報告所有實際運行了的數據競態。它只能檢測到那些在運行時發生的競態,無法用來保證肯定不會發生京態。

  有興趣的可以仔細研究。  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM