溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

GO語言協(xié)程互斥鎖Mutex和讀寫鎖RWMutex怎么用

發(fā)布時間:2022-04-18 10:14:29 來源:億速云 閱讀:121 作者:iii 欄目:開發(fā)技術(shù)

本文小編為大家詳細(xì)介紹“GO語言協(xié)程互斥鎖Mutex和讀寫鎖RWMutex怎么用”,內(nèi)容詳細(xì),步驟清晰,細(xì)節(jié)處理妥當(dāng),希望這篇“GO語言協(xié)程互斥鎖Mutex和讀寫鎖RWMutex怎么用”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學(xué)習(xí)新知識吧。

sync.Mutex

Go中使用sync.Mutex類型實現(xiàn)mutex(排他鎖、互斥鎖)。在源代碼的sync/mutex.go文件中,有如下定義:

// A Mutex is a mutual exclusion lock.
// The zero value for a Mutex is an unlocked mutex.
//
// A Mutex must not be copied after first use.
type Mutex struct {
	state int32
	sema uint32
}

這沒有任何非凡的地方。和mutex相關(guān)的所有事情都是通過sync.Mutex類型的兩個方法sync.Lock()和sync.Unlock()函數(shù)來完成的,前者用于獲取sync.Mutex鎖,后者用于釋放sync.Mutex鎖。sync.Mutex一旦被鎖住,其它的Lock()操作就無法再獲取它的鎖,只有通過Unlock()釋放鎖之后才能通過Lock()繼續(xù)獲取鎖。

也就是說,已有的鎖會導(dǎo)致其它申請Lock()操作的goroutine被阻塞,且只有在Unlock()的時候才會解除阻塞

另外需要注意,sync.Mutex不區(qū)分讀寫鎖,只有Lock()與Lock()之間才會導(dǎo)致阻塞的情況,如果在一個地方Lock(),在另一個地方不Lock()而是直接修改或訪問共享數(shù)據(jù),這對于sync.Mutex類型來說是允許的,因為mutex不會和goroutine進(jìn)行關(guān)聯(lián)。如果想要區(qū)分讀、寫鎖,可以使用sync.RWMutex類型,見后文。

在Lock()和Unlock()之間的代碼段稱為資源的臨界區(qū)(critical section),在這一區(qū)間內(nèi)的代碼是嚴(yán)格被Lock()保護(hù)的,是線程安全的,任何一個時間點(diǎn)都只能有一個goroutine執(zhí)行這段區(qū)間的代碼。

以下是使用sync.Mutex的一個示例,稍后是非常詳細(xì)的分析過程。

package main

import (
	"fmt"
	"sync"
	"time"
)

// 共享變量
var (
	m  sync.Mutex
	v1 int
)

// 修改共享變量
// 在Lock()和Unlock()之間的代碼部分是臨界區(qū)
func change(i int) {
	m.Lock()
	time.Sleep(time.Second)
	v1 = v1 + 1
	if v1%10 == 0 {
		v1 = v1 - 10*i
	}
	m.Unlock()
}

// 訪問共享變量
// 在Lock()和Unlock()之間的代碼部分是是臨界區(qū)
func read() int {
	m.Lock()
	a := v1
	m.Unlock()
	return a
}

func main() {
	var numGR = 21
	var wg sync.WaitGroup

	fmt.Printf("%d", read())

	// 循環(huán)創(chuàng)建numGR個goroutine
	// 每個goroutine都執(zhí)行change()、read()
	// 每個change()和read()都會持有鎖
	for i := 0; i < numGR; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			change(i)
			fmt.Printf(" -> %d", read())
		}(i)
	}

	wg.Wait()
}

第一次執(zhí)行結(jié)果:

0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> -100 -> -99
-> -98 -> -97 -> -96 -> -95 -> -94 -> -93 -> -92 -> -91 -> -260 -> -259

第二次執(zhí)行結(jié)果:注意其中的-74和-72之間跨了一個數(shù)

0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> -80 -> -79 
-> -78 -> -77 -> -76 -> -75 -> -74 -> -72 -> -71 -> -230 -> -229 -> -229

上面的示例中,change()、read()都會申請鎖,并在準(zhǔn)備執(zhí)行完函數(shù)時釋放鎖,它們?nèi)绾涡薷臄?shù)據(jù)、訪問數(shù)據(jù)本文不多做解釋。需要詳細(xì)解釋的是main()中的for循環(huán)部分。

在for循環(huán)中,會不斷激活新的goroutine(共21個)執(zhí)行匿名函數(shù),在每個匿名函數(shù)中都會執(zhí)行change()和read(),意味著每個goroutine都會申請兩次鎖、釋放兩次鎖,且for循環(huán)中沒有任何Sleep延遲,這21個goroutine幾乎是一瞬間同時激活的。

但由于change()和read()中都申請鎖,對于這21個goroutine將要分別執(zhí)行的42個critical section,Lock()保證了在某一時間點(diǎn)只有其中一個goroutine能訪問其中一個critical section。當(dāng)釋放了一個critical section,其它的Lock()將爭奪互斥鎖,也就是所謂的競爭現(xiàn)象(race condition)。因為競爭的存在,這42個critical section被訪問的順序是隨機(jī)的,完全無法保證哪個critical section先被訪問。

對于前9個被調(diào)度到的goroutine,無論是哪個goroutine取得這9個change(i)中的critical section,都只是對共享變量v1做加1運(yùn)算,但當(dāng)?shù)?0個goroutine被調(diào)度時,由于v1加1之后得到10,它滿足if條件,會執(zhí)行v1 = v1 - i*10,但這個i可能是任意0到numGR之間的值(因為無法保證并發(fā)的goroutine的調(diào)度順序),這使得v1的值從第10個goroutine開始出現(xiàn)隨機(jī)性。但從第10到第19個goroutine被調(diào)度的過程中,也只是對共享變量v1做加1運(yùn)算,這些值是可以根據(jù)第10個數(shù)推斷出來的,到第20個goroutine,又再次隨機(jī)。依此類推。

此外,每個goroutine中的read()也都會參與鎖競爭,所以并不能保證每次change(i)之后會隨之執(zhí)行到read(),可能goroutine 1的change()執(zhí)行完后,會跳轉(zhuǎn)到goroutine 3的change()上,這樣一來,goroutine 1的read()就無法讀取到goroutine 1所修改的v1值,而是訪問到其它goroutine中修改后的值。所以,前面的第二次執(zhí)行結(jié)果中出現(xiàn)了一次數(shù)據(jù)跨越。只不過執(zhí)行完change()后立即執(zhí)行read()的幾率比較大,所以多數(shù)時候輸出的數(shù)據(jù)都是連續(xù)的。

總而言之,Mutex保證了每個critical section安全,某一時間點(diǎn)只有一個goroutine訪問到這部分,但也因此而出現(xiàn)了隨機(jī)性。

如果Lock()后忘記了Unlock(),將會永久阻塞而出現(xiàn)死鎖。如果

適合sync.Mutex的數(shù)據(jù)類型

其實,對于內(nèi)置類型的共享變量來說,使用sync.Mutex和Lock()、Unlock()來保護(hù)也是不合理的,因為它們自身不包含Mutex屬性。真正合理的共享變量是那些包含Mutex屬性的struct類型。例如:

type mytype struct {
	m   sync.Mutex
	var int
}

x := new(mytype)

這時只要想保護(hù)var變量,就先x.m.Lock(),操作完var后,再x.m.Unlock()。這樣就能保證x中的var字段變量一定是被保護(hù)的。

sync.RWMutex

Go中使用sync.RWMutex類型實現(xiàn)讀寫互斥鎖rwmutex。在源代碼的sync/rwmutex.go文件中,有如下定義:

// A RWMutex is a reader/writer mutual exclusion lock.
// The lock can be held by an arbitrary number of readers or a single writer.
// The zero value for a RWMutex is an unlocked mutex.
//
// A RWMutex must not be copied after first use.
//
// If a goroutine holds a RWMutex for reading and another goroutine might
// call Lock, no goroutine should expect to be able to acquire a read lock
// until the initial read lock is released. In particular, this prohibits
// recursive read locking. This is to ensure that the lock eventually becomes
// available; a blocked Lock call excludes new readers from acquiring the
// lock.
type RWMutex struct {
	w           Mutex  // held if there are pending writers
	writerSem   uint32 // 寫鎖需要等待讀鎖釋放的信號量
	readerSem   uint32 // 讀鎖需要等待寫鎖釋放的信號量
	readerCount int32  // 讀鎖后面掛起了多少個寫鎖申請
	readerWait  int32  // 已釋放了多少個讀鎖
}

上面的注釋和源代碼說明了幾點(diǎn):

  • RWMutex是基于Mutex的,在Mutex的基礎(chǔ)之上增加了讀、寫的信號量,并使用了類似引用計數(shù)的讀鎖數(shù)量

  • 讀鎖與讀鎖兼容,讀鎖與寫鎖互斥,寫鎖與寫鎖互斥,只有在鎖釋放后才可以繼續(xù)申請互斥的鎖

    • 可以同時申請多個讀鎖

    • 有讀鎖時申請寫鎖將阻塞,有寫鎖時申請讀鎖將阻塞

    • 只要有寫鎖,后續(xù)申請讀鎖和寫鎖都將阻塞

此類型有幾個鎖和解鎖的方法:

func (rw *RWMutex) Lock()
func (rw *RWMutex) RLock()
func (rw *RWMutex) RLocker() Locker
func (rw *RWMutex) RUnlock()
func (rw *RWMutex) Unlock()

其中:

  • Lock()和Unlock()用于申請和釋放寫鎖

  • RLock()和RUnlock()用于申請和釋放讀鎖

    • 一次RUnlock()操作只是對讀鎖數(shù)量減1,即減少一次讀鎖的引用計數(shù)

  • 如果不存在寫鎖,則Unlock()引發(fā)panic,如果不存在讀鎖,則RUnlock()引發(fā)panic

  • RLocker()用于返回一個實現(xiàn)了Lock()和Unlock()方法的Locker接口

此外,無論是Mutex還是RWMutex都不會和goroutine進(jìn)行關(guān)聯(lián),這意味著它們的鎖申請行為可以在一個goroutine中操作,釋放鎖行為可以在另一個goroutine中操作。

由于RLock()和Lock()都能保證數(shù)據(jù)不被其它goroutine修改,所以在RLock()與RUnlock()之間的,以及Lock()與Unlock()之間的代碼區(qū)都是critical section。

以下是一個示例,此示例中同時使用了Mutex和RWMutex,RWMutex用于讀、寫,Mutex只用于讀。

package main

import (
	"fmt"
	"os"
	"sync"
	"time"
)

var Password = secret{password: "myPassword"}

type secret struct {
	RWM      sync.RWMutex
	M        sync.Mutex
	password string
}

// 通過rwmutex寫
func Change(c *secret, pass string) {
	c.RWM.Lock()
	fmt.Println("Change with rwmutex lock")
	time.Sleep(3 * time.Second)
	c.password = pass
	c.RWM.Unlock()
}

// 通過rwmutex讀
func rwMutexShow(c *secret) string {
	c.RWM.RLock()
	fmt.Println("show with rwmutex",time.Now().Second())
	time.Sleep(1 * time.Second)
	defer c.RWM.RUnlock()
	return c.password
}

// 通過mutex讀,和rwMutexShow的唯一區(qū)別在于鎖的方式不同
func mutexShow(c *secret) string {
	c.M.Lock()
	fmt.Println("show with mutex:",time.Now().Second())
	time.Sleep(1 * time.Second)
	defer c.M.Unlock()
	return c.password
}

func main() {
	// 定義一個稍后用于覆蓋(重寫)的函數(shù)
	var show = func(c *secret) string { return "" }

	// 通過變量賦值的方式,選擇并重寫showFunc函數(shù)
	if len(os.Args) != 2 {
		fmt.Println("Using sync.RWMutex!",time.Now().Second())
		show = rwMutexShow
	} else {
		fmt.Println("Using sync.Mutex!",time.Now().Second())
		show = mutexShow
	}
	
	var wg sync.WaitGroup

	// 激活5個goroutine,每個goroutine都查看
	// 根據(jù)選擇的函數(shù)不同,showFunc()加鎖的方式不同
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			fmt.Println("Go Pass:", show(&Password),time.Now().Second())
		}()
	}
	
	// 激活一個申請寫鎖的goroutine
	go func() {
		wg.Add(1)
		defer wg.Done()
		Change(&Password, "123456")
	}()
	// 阻塞,直到所有wg.Done
	wg.Wait()
}

Change()函數(shù)申請寫鎖,并睡眠3秒后修改數(shù)據(jù),然后釋放寫鎖。

rwMutexShow()函數(shù)申請讀鎖,并睡眠一秒后取得數(shù)據(jù),并釋放讀鎖。注意,rwMutexShow()中的print和return是相隔一秒鐘的。

mutexShow()函數(shù)申請Mutex鎖,和RWMutex互不相干。和rwMutexShow()唯一不同之處在于申請的鎖不同。

main()中,先根據(jù)命令行參數(shù)數(shù)量決定運(yùn)行哪一個show()。之所以能根據(jù)函數(shù)變量來賦值,是因為先定義了一個show()函數(shù),它的函數(shù)簽名和rwMutexShow()、mutexShow()的簽名相同,所以可以相互賦值。

for循環(huán)中激活了5個goroutine并發(fā)運(yùn)行,for瞬間激活5個goroutine后,繼續(xù)執(zhí)行main()代碼會激活另一個用于申請寫鎖的goroutine。這6個goroutine的執(zhí)行順序是隨機(jī)的。

如果show選中的函數(shù)是rwMutexShow(),則5個goroutine要申請的RLock()鎖和寫鎖是沖突的,但5個RLock()是兼容的。所以,只要某個時間點(diǎn)調(diào)度到了寫鎖的goroutine,剩下的讀鎖goroutine都會從那時開始阻塞3秒。

除此之外,還有一個不嚴(yán)格準(zhǔn)確,但在時間持續(xù)長短的理論上來說能保證的一個規(guī)律:當(dāng)修改數(shù)據(jù)結(jié)束后,各個剩下的goroutine都申請讀鎖,因為申請后立即print輸出,然后睡眠1秒,但1秒時間足夠所有剩下的goroutine申請完讀鎖,使得show with rwmutex輸出是連在一起,輸出的Go Pass: 123456又是連在一起的。

某次結(jié)果如下:

Using sync.RWMutex! 58
show with rwmutex 58
Change with rwmutex lock
Go Pass: myPassword 59
show with rwmutex 2
show with rwmutex 2
show with rwmutex 2
show with rwmutex 2
Go Pass: 123456 3
Go Pass: 123456 3
Go Pass: 123456 3
Go Pass: 123456 3

如果show選中的函數(shù)是mutexShow(),則讀數(shù)據(jù)和寫數(shù)據(jù)互不沖突,但讀和讀是沖突的(因為Mutex的Lock()是互斥的)。

某次結(jié)果如下:

Using sync.Mutex! 30
Change with rwmutex lock
show with mutex: 30
Go Pass: myPassword 31
show with mutex: 31
Go Pass: myPassword 32
show with mutex: 32
Go Pass: 123456 33
show with mutex: 33
show with mutex: 34
Go Pass: 123456 34
Go Pass: 123456 35

用Mutex還是用RWMutex

Mutex和RWMutex都不關(guān)聯(lián)goroutine,但RWMutex顯然更適用于讀多寫少的場景。僅針對讀的性能來說,RWMutex要高于Mutex,因為rwmutex的多個讀可以并存。

讀到這里,這篇“GO語言協(xié)程互斥鎖Mutex和讀寫鎖RWMutex怎么用”文章已經(jīng)介紹完畢,想要掌握這篇文章的知識點(diǎn)還需要大家自己動手實踐使用過才能領(lǐng)會,如果想了解更多相關(guān)內(nèi)容的文章,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI