国产片侵犯亲女视频播放_亚洲精品二区_在线免费国产视频_欧美精品一区二区三区在线_少妇久久久_在线观看av不卡

腳本之家,腳本語(yǔ)言編程技術(shù)及教程分享平臺(tái)!
分類(lèi)導(dǎo)航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|

服務(wù)器之家 - 腳本之家 - Golang - GO語(yǔ)言并發(fā)編程之互斥鎖、讀寫(xiě)鎖詳解

GO語(yǔ)言并發(fā)編程之互斥鎖、讀寫(xiě)鎖詳解

2020-04-10 14:33junjie Golang

這篇文章主要介紹了GO語(yǔ)言并發(fā)編程之互斥鎖、讀寫(xiě)鎖詳解,本文是GO并發(fā)編程實(shí)戰(zhàn)一書(shū)的樣章,詳細(xì)講解了互斥鎖、讀寫(xiě)鎖,然后給出了一個(gè)完整示例,需要的朋友可以參考下

在本節(jié),我們對(duì)Go語(yǔ)言所提供的與鎖有關(guān)的API進(jìn)行說(shuō)明。這包括了互斥鎖和讀寫(xiě)鎖。我們?cè)诘?章描述過(guò)互斥鎖,但卻沒(méi)有提到過(guò)讀寫(xiě)鎖。這兩種鎖對(duì)于傳統(tǒng)的并發(fā)程序來(lái)說(shuō)都是非常常用和重要的。

一、互斥鎖

互斥鎖是傳統(tǒng)的并發(fā)程序?qū)蚕碣Y源進(jìn)行訪問(wèn)控制的主要手段。它由標(biāo)準(zhǔn)庫(kù)代碼包sync中的Mutex結(jié)構(gòu)體類(lèi)型代表。sync.Mutex類(lèi)型(確切地說(shuō),是*sync.Mutex類(lèi)型)只有兩個(gè)公開(kāi)方法——Lock和Unlock。顧名思義,前者被用于鎖定當(dāng)前的互斥量,而后者則被用來(lái)對(duì)當(dāng)前的互斥量進(jìn)行解鎖。

類(lèi)型sync.Mutex的零值表示了未被鎖定的互斥量。也就是說(shuō),它是一個(gè)開(kāi)箱即用的工具。我們只需對(duì)它進(jìn)行簡(jiǎn)單聲明就可以正常使用了,就像這樣:

 

復(fù)制代碼 代碼如下:


var mutex sync.Mutex

 

mutex.Lock()

 

在我們使用其他編程語(yǔ)言(比如C或Java)的鎖類(lèi)工具的時(shí)候,可能會(huì)犯的一個(gè)低級(jí)錯(cuò)誤就是忘記及時(shí)解開(kāi)已被鎖住的鎖,從而導(dǎo)致諸如流程執(zhí)行異常、線程執(zhí)行停滯甚至程序死鎖等等一系列問(wèn)題的發(fā)生。然而,在Go語(yǔ)言中,這個(gè)低級(jí)錯(cuò)誤的發(fā)生幾率極低。其主要原因是有defer語(yǔ)句的存在。

我們一般會(huì)在鎖定互斥鎖之后緊接著就用defer語(yǔ)句來(lái)保證該互斥鎖的及時(shí)解鎖。請(qǐng)看下面這個(gè)函數(shù):

復(fù)制代碼 代碼如下:


var mutex sync.Mutex

 

func write() {

mutex.Lock()

defer mutex.Unlock()

// 省略若干條語(yǔ)句

}

 

函數(shù)write中的這條defer語(yǔ)句保證了在該函數(shù)被執(zhí)行結(jié)束之前互斥鎖mutex一定會(huì)被解鎖。這省去了我們?cè)谒衦eturn語(yǔ)句之前以及異常發(fā)生之時(shí)重復(fù)的附加解鎖操作的工作。在函數(shù)的內(nèi)部執(zhí)行流程相對(duì)復(fù)雜的情況下,這個(gè)工作量是不容忽視的,并且極易出現(xiàn)遺漏和導(dǎo)致錯(cuò)誤。所以,這里的defer語(yǔ)句總是必要的。在Go語(yǔ)言中,這是很重要的一個(gè)慣用法。我們應(yīng)該養(yǎng)成這種良好的習(xí)慣。

對(duì)于同一個(gè)互斥鎖的鎖定操作和解鎖操作總是應(yīng)該成對(duì)的出現(xiàn)。如果我們鎖定了一個(gè)已被鎖定的互斥鎖,那么進(jìn)行重復(fù)鎖定操作的Goroutine將會(huì)被阻塞,直到該互斥鎖回到解鎖狀態(tài)。請(qǐng)看下面的示例:

 

復(fù)制代碼 代碼如下:


func repeatedlyLock() {

 

var mutex sync.Mutex

fmt.Println("Lock the lock. (G0)")

mutex.Lock()

fmt.Println("The lock is locked. (G0)")

for i := 1; i <= 3; i++ {

go func(i int) {

fmt.Printf("Lock the lock. (G%d)\n", i)

mutex.Lock()

fmt.Printf("The lock is locked. (G%d)\n", i)

}(i)

}

time.Sleep(time.Second)

fmt.Println("Unlock the lock. (G0)")

mutex.Unlock()

fmt.Println("The lock is unlocked. (G0)")

time.Sleep(time.Second)

}

 

我們把執(zhí)行repeatedlyLock函數(shù)的Goroutine稱(chēng)為G0。而在repeatedlyLock函數(shù)中,我們又啟用了3個(gè)Goroutine,并分別把它們命名為G1、G2和G3。可以看到,我們?cè)趩⒂眠@3個(gè)Goroutine之前就已經(jīng)對(duì)互斥鎖mutex進(jìn)行了鎖定,并且在這3個(gè)Goroutine將要執(zhí)行的go函數(shù)的開(kāi)始處也加入了對(duì)mutex的鎖定操作。這樣做的意義是模擬并發(fā)地對(duì)同一個(gè)互斥鎖進(jìn)行鎖定的情形。當(dāng)for語(yǔ)句被執(zhí)行完畢之后,我們先讓G0小睡1秒鐘,以使運(yùn)行時(shí)系統(tǒng)有充足的時(shí)間開(kāi)始運(yùn)行G1、G2和G3。在這之后,解鎖mutex。為了能夠讓讀者更加清晰地了解到repeatedlyLock函數(shù)被執(zhí)行的情況,我們?cè)谶@些鎖定和解鎖操作的前后加入了若干條打印語(yǔ)句,并在打印內(nèi)容中添加了我們?yōu)檫@幾個(gè)Goroutine起的名字。也由于這個(gè)原因,我們?cè)趓epeatedlyLock函數(shù)的最后再次編寫(xiě)了一條“睡眠”語(yǔ)句,以此為可能出現(xiàn)的其他打印內(nèi)容再等待一小會(huì)兒。

經(jīng)過(guò)短暫的執(zhí)行,標(biāo)準(zhǔn)輸出上會(huì)出現(xiàn)如下內(nèi)容:

 

復(fù)制代碼 代碼如下:


Lock the lock. (G0)

 

The lock is locked. (G0)

Lock the lock. (G1)

Lock the lock. (G2)

Lock the lock. (G3)

Unlock the lock. (G0)

The lock is unlocked. (G0)

The lock is locked. (G1)

 

從這八行打印內(nèi)容中,我們可以清楚的看出上述四個(gè)Goroutine的執(zhí)行情況。首先,在repeatedlyLock函數(shù)被執(zhí)行伊始,對(duì)互斥鎖的第一次鎖定操作便被進(jìn)行并順利地完成。這由第一行和第二行打印內(nèi)容可以看出。而后,在repeatedlyLock函數(shù)中被啟用的那三個(gè)Goroutine在G0的第一次“睡眠”期間開(kāi)始被運(yùn)行。當(dāng)相應(yīng)的go函數(shù)中的對(duì)互斥鎖的鎖定操作被進(jìn)行的時(shí)候,它們都被阻塞住了。原因是該互斥鎖已處于鎖定狀態(tài)了。這就是我們?cè)谶@里只看到了三個(gè)連續(xù)的Lock the lock. (G<i>)而沒(méi)有立即看到The lock is locked. (G<i>)的原因。隨后,G0“睡醒”并解鎖互斥鎖。這使得正在被阻塞的G1、G2和G3都會(huì)有機(jī)會(huì)重新鎖定該互斥鎖。但是,只有一個(gè)Goroutine會(huì)成功。成功完成鎖定操作的某一個(gè)Goroutine會(huì)繼續(xù)執(zhí)行在該操作之后的語(yǔ)句。而其他Goroutine將繼續(xù)被阻塞,直到有新的機(jī)會(huì)到來(lái)。這也就是上述打印內(nèi)容中的最后三行所表達(dá)的含義。顯然,G1搶到了這次機(jī)會(huì)并成功鎖定了那個(gè)互斥鎖。

實(shí)際上,我們之所以能夠通過(guò)使用互斥鎖對(duì)共享資源的唯一性訪問(wèn)進(jìn)行控制正是因?yàn)樗倪@一特性。這有效的對(duì)競(jìng)態(tài)條件進(jìn)行了消除。

互斥鎖的鎖定操作的逆操作并不會(huì)引起任何Goroutine的阻塞。但是,它的進(jìn)行有可能引發(fā)運(yùn)行時(shí)恐慌。更確切的講,當(dāng)我們對(duì)一個(gè)已處于解鎖狀態(tài)的互斥鎖進(jìn)行解鎖操作的時(shí)候,就會(huì)已發(fā)一個(gè)運(yùn)行時(shí)恐慌。這種情況很可能會(huì)出現(xiàn)在相對(duì)復(fù)雜的流程之中——我們可能會(huì)在某個(gè)或多個(gè)分支中重復(fù)的加入針對(duì)同一個(gè)互斥鎖的解鎖操作。避免這種情況發(fā)生的最簡(jiǎn)單、有效的方式依然是使用defer語(yǔ)句。這樣更容易保證解鎖操作的唯一性。

雖然互斥鎖可以被直接的在多個(gè)Goroutine之間共享,但是我們還是強(qiáng)烈建議把對(duì)同一個(gè)互斥鎖的成對(duì)的鎖定和解鎖操作放在同一個(gè)層次的代碼塊中。例如,在同一個(gè)函數(shù)或方法中對(duì)某個(gè)互斥鎖的進(jìn)行鎖定和解鎖。又例如,把互斥鎖作為某一個(gè)結(jié)構(gòu)體類(lèi)型中的字段,以便在該類(lèi)型的多個(gè)方法中使用它。此外,我們還應(yīng)該使代表互斥鎖的變量的訪問(wèn)權(quán)限盡量的低。這樣才能盡量避免它在不相關(guān)的流程中被誤用,從而導(dǎo)致程序不正確的行為。

互斥鎖是我們見(jiàn)到過(guò)的眾多同步工具中最簡(jiǎn)單的一個(gè)。只要遵循前面提及的幾個(gè)小技巧,我們就可以以正確、高效的方式使用互斥鎖,并用它來(lái)確保對(duì)共享資源的訪問(wèn)的唯一性。下面我們來(lái)看看稍微復(fù)雜一些的鎖實(shí)現(xiàn)——讀寫(xiě)鎖。

二、讀寫(xiě)鎖

讀寫(xiě)鎖即是針對(duì)于讀寫(xiě)操作的互斥鎖。它與普通的互斥鎖最大的不同就是,它可以分別針對(duì)讀操作和寫(xiě)操作進(jìn)行鎖定和解鎖操作。讀寫(xiě)鎖遵循的訪問(wèn)控制規(guī)則與互斥鎖有所不同。在讀寫(xiě)鎖管轄的范圍內(nèi),它允許任意個(gè)讀操作的同時(shí)進(jìn)行。但是,在同一時(shí)刻,它只允許有一個(gè)寫(xiě)操作在進(jìn)行。并且,在某一個(gè)寫(xiě)操作被進(jìn)行的過(guò)程中,讀操作的進(jìn)行也是不被允許的。也就是說(shuō),讀寫(xiě)鎖控制下的多個(gè)寫(xiě)操作之間都是互斥的,并且寫(xiě)操作與讀操作之間也都是互斥的。但是,多個(gè)讀操作之間卻不存在互斥關(guān)系。

這樣的規(guī)則對(duì)于針對(duì)同一塊數(shù)據(jù)的并發(fā)讀寫(xiě)來(lái)講是非常貼切的。因?yàn)椋瑹o(wú)論讀操作的并發(fā)量有多少,這些操作都不會(huì)對(duì)數(shù)據(jù)本身造成變更。而寫(xiě)操作不但會(huì)對(duì)同時(shí)進(jìn)行的其他寫(xiě)操作進(jìn)行干擾,還有可能造成同時(shí)進(jìn)行的讀操作的結(jié)果的不正確。例如,在32位的操作系統(tǒng)中,針對(duì)int64類(lèi)型值的讀操作和寫(xiě)操作都不可能只由一個(gè)CPU指令完成。在一個(gè)寫(xiě)操作被進(jìn)行的過(guò)程當(dāng)中,針對(duì)同一個(gè)只的讀操作可能會(huì)讀取到未被修改完成的值。該值既不與舊的值相等,也不等于新的值。這種錯(cuò)誤往往不易被發(fā)現(xiàn),且很難被修正。因此,在這樣的場(chǎng)景下,讀寫(xiě)鎖可以在大大降低因使用鎖而對(duì)程序性能造成的損耗的情況下完成對(duì)共享資源的訪問(wèn)控制。

在Go語(yǔ)言中,讀寫(xiě)鎖由結(jié)構(gòu)體類(lèi)型sync.RWMutex代表。與互斥鎖類(lèi)似,sync.RWMutex類(lèi)型的零值就已經(jīng)是立即可用的讀寫(xiě)鎖了。在此類(lèi)型的方法集合中包含了兩對(duì)方法,即:

 

復(fù)制代碼 代碼如下:


func (*RWMutex) Lock

 

func (*RWMutex) Unlock

 

 

復(fù)制代碼 代碼如下:


func (*RWMutex) RLock

 

func (*RWMutex) RUnlock

 

前一對(duì)方法的名稱(chēng)和簽名與互斥鎖的那兩個(gè)方法完全一致。它們分別代表了對(duì)寫(xiě)操作的鎖定和解鎖。以下簡(jiǎn)稱(chēng)它們?yōu)閷?xiě)鎖定和寫(xiě)解鎖。而后一對(duì)方法則分別表示了對(duì)讀操作的鎖定和解鎖。以下簡(jiǎn)稱(chēng)它們?yōu)樽x鎖定和讀解鎖。

對(duì)已被寫(xiě)鎖定的讀寫(xiě)鎖進(jìn)行寫(xiě)鎖定,會(huì)造成當(dāng)前Goroutine的阻塞,直到該讀寫(xiě)鎖被寫(xiě)解鎖。當(dāng)然,如果有多個(gè)Goroutine因此而被阻塞,那么當(dāng)對(duì)應(yīng)的寫(xiě)解鎖被進(jìn)行之時(shí)只會(huì)使其中一個(gè)Goroutine的運(yùn)行被恢復(fù)。類(lèi)似的,對(duì)一個(gè)已被寫(xiě)鎖定的讀寫(xiě)鎖進(jìn)行讀鎖定,也會(huì)阻塞相應(yīng)的Goroutine。但不同的是,一旦該讀寫(xiě)鎖被寫(xiě)解鎖,那么所有因欲進(jìn)行讀鎖定而被阻塞的Goroutine的運(yùn)行都會(huì)被恢復(fù)。另一方面,如果在進(jìn)行過(guò)程中發(fā)現(xiàn)當(dāng)前的讀寫(xiě)鎖已被讀鎖定,那么這個(gè)寫(xiě)鎖定操作將會(huì)等待直至所有施加于該讀寫(xiě)鎖之上的讀鎖定都被清除。同樣的,在有多個(gè)寫(xiě)鎖定操作為此而等待的情況下,相應(yīng)的讀鎖定的全部清除只能讓其中的某一個(gè)寫(xiě)鎖定操作獲得進(jìn)行的機(jī)會(huì)。

現(xiàn)在來(lái)關(guān)注寫(xiě)解鎖和讀解鎖。如果對(duì)一個(gè)未被寫(xiě)鎖定的讀寫(xiě)鎖進(jìn)行寫(xiě)解鎖,那么會(huì)引發(fā)一個(gè)運(yùn)行時(shí)恐慌。類(lèi)似的,當(dāng)對(duì)一個(gè)未被讀鎖定的讀寫(xiě)鎖進(jìn)行讀解鎖的時(shí)候也會(huì)引發(fā)一個(gè)運(yùn)行時(shí)恐慌。寫(xiě)解鎖在進(jìn)行的同時(shí)會(huì)試圖喚醒所有因進(jìn)行讀鎖定而被阻塞的Goroutine。而讀解鎖在進(jìn)行的時(shí)候則會(huì)試圖喚醒一個(gè)因進(jìn)行寫(xiě)鎖定而被阻塞的Goroutine。

無(wú)論鎖定針對(duì)的是寫(xiě)操作還是讀操作,我們都應(yīng)該盡量及時(shí)的對(duì)相應(yīng)的鎖進(jìn)行解鎖。對(duì)于寫(xiě)解鎖,我們自不必多說(shuō)。而讀解鎖的及時(shí)進(jìn)行往往更容易被我們忽視。雖說(shuō)讀解鎖的進(jìn)行并不會(huì)對(duì)其他正在進(jìn)行中的讀操作產(chǎn)生任何影響,但它卻與相應(yīng)的寫(xiě)鎖定的進(jìn)行關(guān)系緊密。注意,對(duì)于同一個(gè)讀寫(xiě)鎖來(lái)說(shuō),施加在它之上的讀鎖定可以有多個(gè)。因此,只有我們對(duì)互斥鎖進(jìn)行相同數(shù)量的讀解鎖,才能夠讓某一個(gè)相應(yīng)的寫(xiě)鎖定獲得進(jìn)行的機(jī)會(huì)。否則,后者會(huì)繼續(xù)使進(jìn)行它的Goroutine處于阻塞狀態(tài)。由于sync.RWMutex和*sync.RWMutex類(lèi)型都沒(méi)有相應(yīng)的方法讓我們獲得已進(jìn)行的讀鎖定的數(shù)量,所以這里是很容易出現(xiàn)問(wèn)題的。還好我們可以使用defer語(yǔ)句來(lái)盡量避免此類(lèi)問(wèn)題的發(fā)生。請(qǐng)記住,針對(duì)同一個(gè)讀寫(xiě)鎖的寫(xiě)鎖定和讀鎖定是互斥的。無(wú)論是寫(xiě)解鎖還是讀解鎖,操作的不及時(shí)都會(huì)對(duì)使用該讀寫(xiě)鎖的流程的正常執(zhí)行產(chǎn)生負(fù)面影響。

除了我們?cè)谇懊嬖敿?xì)講解的那兩對(duì)方法之外,*sync.RWMutex類(lèi)型還擁有另外一個(gè)方法——RLocker。這個(gè)RLocker方法會(huì)返回一個(gè)實(shí)現(xiàn)了sync.Locker接口的值。sync.Locker接口類(lèi)型包含了兩個(gè)方法,即:Lock和Unlock。細(xì)心的讀者可能會(huì)發(fā)現(xiàn),*sync.Mutex類(lèi)型和*sync.RWMutex類(lèi)型都是該接口類(lèi)型的實(shí)現(xiàn)類(lèi)型。實(shí)際上,我們?cè)谡{(diào)用*sync.RWMutex類(lèi)型值的RLocker方法之后所得到的結(jié)果值就是這個(gè)值本身。只不過(guò),這個(gè)結(jié)果值的Lock方法和Unlock方法分別對(duì)應(yīng)了針對(duì)該讀寫(xiě)鎖的讀鎖定操作和讀解鎖操作。換句話說(shuō),我們?cè)趯?duì)一個(gè)讀寫(xiě)鎖的RLocker方法的結(jié)果值的Lock方法或Unlock方法進(jìn)行調(diào)用的時(shí)候?qū)嶋H上是在調(diào)用該讀寫(xiě)鎖的RLock方法或RUnlock方法。這樣的操作適配在實(shí)現(xiàn)上并不困難。我們自己也可以很容易的編寫(xiě)出這些方法的實(shí)現(xiàn)。通過(guò)讀寫(xiě)鎖的RLocker方法獲得這樣一個(gè)結(jié)果值的實(shí)際意義在于,我們可以在之后以相同的方式對(duì)該讀寫(xiě)鎖中的“寫(xiě)鎖”和“讀鎖”進(jìn)行操作。這為相關(guān)操作的靈活適配和替換提供了方便。

三、鎖的完整示例

我們下面來(lái)看一個(gè)與上述鎖實(shí)現(xiàn)有關(guān)的示例。在Go語(yǔ)言的標(biāo)準(zhǔn)庫(kù)代碼包os中有一個(gè)名為File的結(jié)構(gòu)體類(lèi)型。os.File類(lèi)型的值可以被用來(lái)代表文件系統(tǒng)中的某一個(gè)文件或目錄。它的方法集合中包含了很多方法,其中的一些方法被用來(lái)對(duì)相應(yīng)的文件進(jìn)行寫(xiě)操作和讀操作。

假設(shè),我們需要?jiǎng)?chuàng)建一個(gè)文件來(lái)存放數(shù)據(jù)。在同一個(gè)時(shí)刻,可能會(huì)有多個(gè)Goroutine分別進(jìn)行對(duì)此文件的進(jìn)行寫(xiě)操作和讀操作。每一次寫(xiě)操作都應(yīng)該向這個(gè)文件寫(xiě)入若干個(gè)字節(jié)的數(shù)據(jù)。這若干字節(jié)的數(shù)據(jù)應(yīng)該作為一個(gè)獨(dú)立的數(shù)據(jù)塊存在。這就意味著,寫(xiě)操作之間不能彼此干擾,寫(xiě)入的內(nèi)容之間也不能出現(xiàn)穿插和混淆的情況。另一方面,每一次讀操作都應(yīng)該從這個(gè)文件中讀取一個(gè)獨(dú)立、完整的數(shù)據(jù)塊。它們讀取的數(shù)據(jù)塊不能重復(fù),且需要按順序讀取。例如,第一個(gè)讀操作讀取了數(shù)據(jù)塊1,那么第二個(gè)讀操作就應(yīng)該去讀取數(shù)據(jù)塊2,而第三個(gè)讀操作則應(yīng)該讀取數(shù)據(jù)塊3,以此類(lèi)推。對(duì)于這些讀操作是否可以被同時(shí)進(jìn)行,這里并不做要求。即使它們被同時(shí)進(jìn)行,程序也應(yīng)該分辨出它們的先后順序。

為了突出重點(diǎn),我們規(guī)定每個(gè)數(shù)據(jù)塊的長(zhǎng)度都是相同的。該長(zhǎng)度應(yīng)該在初始化的時(shí)候被給定。若寫(xiě)操作實(shí)際欲寫(xiě)入數(shù)據(jù)的長(zhǎng)度超過(guò)了該值,則超出部分將會(huì)被截掉。

當(dāng)我們拿到這樣一個(gè)需求的時(shí)候,首先應(yīng)該想到使用os.File類(lèi)型。它為我們操作文件系統(tǒng)中的文件提供了底層的支持。但是,該類(lèi)型的相關(guān)方法并沒(méi)有對(duì)并發(fā)操作的安全性進(jìn)行保證。換句話說(shuō),這些方法不是并發(fā)安全的。我只能通過(guò)額外的同步手段來(lái)保證這一點(diǎn)。鑒于這里需要分別對(duì)兩類(lèi)操作(即寫(xiě)操作和讀操作)進(jìn)行訪問(wèn)控制,所以讀寫(xiě)鎖在這里會(huì)比普通的互斥鎖更加適用。不過(guò),關(guān)于多個(gè)讀操作要按順序且不能重復(fù)讀取的這個(gè)問(wèn)題,我們需還要使用其他輔助手段來(lái)解決。

為了實(shí)現(xiàn)上述需求,我們需要?jiǎng)?chuàng)建一個(gè)類(lèi)型。作為該類(lèi)型的行為定義,我們先編寫(xiě)了一個(gè)這樣的接口:

復(fù)制代碼 代碼如下:


// 數(shù)據(jù)文件的接口類(lèi)型。

 

type DataFile interface {

// 讀取一個(gè)數(shù)據(jù)塊。

Read() (rsn int64, d Data, err error)

// 寫(xiě)入一個(gè)數(shù)據(jù)塊。

Write(d Data) (wsn int64, err error)

// 獲取最后讀取的數(shù)據(jù)塊的序列號(hào)。

Rsn() int64

// 獲取最后寫(xiě)入的數(shù)據(jù)塊的序列號(hào)。

Wsn() int64

// 獲取數(shù)據(jù)塊的長(zhǎng)度

DataLen() uint32

}

 

其中,類(lèi)型Data被聲明為一個(gè)[]byte的別名類(lèi)型:

 

復(fù)制代碼 代碼如下:


// 數(shù)據(jù)的類(lèi)型

 

type Data []byte

 

而名稱(chēng)wsn和rsn分別是Writing Serial Number和Reading Serial Number的縮寫(xiě)形式。它們分別代表了最后被寫(xiě)入的數(shù)據(jù)塊的序列號(hào)和最后被讀取的數(shù)據(jù)塊的序列號(hào)。這里所說(shuō)的序列號(hào)相當(dāng)于一個(gè)計(jì)數(shù)值,它會(huì)從1開(kāi)始。因此,我們可以通過(guò)調(diào)用Rsn方法和Wsn方法得到當(dāng)前已被讀取和寫(xiě)入的數(shù)據(jù)塊的數(shù)量。

根據(jù)上面對(duì)需求的簡(jiǎn)單分析和這個(gè)DataFile接口類(lèi)型聲明,我們就可以來(lái)編寫(xiě)真正的實(shí)現(xiàn)了。我們將這個(gè)實(shí)現(xiàn)類(lèi)型命名為myDataFile。它的基本結(jié)構(gòu)如下:

復(fù)制代碼 代碼如下:


// 數(shù)據(jù)文件的實(shí)現(xiàn)類(lèi)型。

 

type myDataFile struct {

f       *os.File     // 文件。

fmutex sync.RWMutex // 被用于文件的讀寫(xiě)鎖。

woffset int64       // 寫(xiě)操作需要用到的偏移量。

roffset int64       // 讀操作需要用到的偏移量。

wmutex sync.Mutex   // 寫(xiě)操作需要用到的互斥鎖。

rmutex sync.Mutex   // 讀操作需要用到的互斥鎖。

dataLen uint32       // 數(shù)據(jù)塊長(zhǎng)度。

}

 

類(lèi)型myDataFile共有七個(gè)字段。我們已經(jīng)在前面說(shuō)明過(guò)前兩個(gè)字段存在的意義。由于對(duì)數(shù)據(jù)文件的寫(xiě)操作和讀操作是各自獨(dú)立的,所以我們需要兩個(gè)字段來(lái)存儲(chǔ)兩類(lèi)操作的進(jìn)行進(jìn)度。在這里,這個(gè)進(jìn)度由偏移量代表。此后,我們把woffset字段稱(chēng)為寫(xiě)偏移量,而把roffset字段稱(chēng)為讀偏移量。注意,我們?cè)谶M(jìn)行寫(xiě)操作和讀操作的時(shí)候會(huì)分別增加這兩個(gè)字段的值。當(dāng)有多個(gè)寫(xiě)操作同時(shí)要增加woffset字段的值的時(shí)候就會(huì)產(chǎn)生競(jìng)態(tài)條件。因此,我們需要互斥鎖wmutex來(lái)對(duì)其加以保護(hù)。類(lèi)似的,rmutex互斥鎖被用來(lái)消除多個(gè)讀操作同時(shí)增加roffset字段的值時(shí)產(chǎn)生的競(jìng)態(tài)條件。最后,由上述的需求可知,數(shù)據(jù)塊的長(zhǎng)度應(yīng)該是在初始化myDataFile類(lèi)型值的時(shí)候被給定的。這個(gè)長(zhǎng)度會(huì)被存儲(chǔ)在該值的dataLen字段中。它與DataFile接口中聲明的DataLen方法是對(duì)應(yīng)的。下面我們就來(lái)看看被用來(lái)創(chuàng)建和初始化DataFile類(lèi)型值的函數(shù)NewDataFile。

關(guān)于這類(lèi)函數(shù)的編寫(xiě),讀者應(yīng)該已經(jīng)駕輕就熟了。NewDataFile函數(shù)會(huì)返回一個(gè)DataFile類(lèi)型值,但是實(shí)際上它會(huì)創(chuàng)建并初始化一個(gè)*myDataFile類(lèi)型的值并把它作為它的結(jié)果值。這樣可以通過(guò)編譯的原因是,后者會(huì)是前者的一個(gè)實(shí)現(xiàn)類(lèi)型。NewDataFile函數(shù)的完整聲明如下:

復(fù)制代碼 代碼如下:


func NewDataFile(path string, dataLen uint32) (DataFile, error) {

 

f, err := os.Create(path)

if err != nil {

return nil, err

}

if dataLen == 0 {

return nil, errors.New("Invalid data length!")

}

df := &myDataFile{f: f, dataLen: dataLen}

return df, nil

}

 

可以看到,我們?cè)趧?chuàng)建*myDataFile類(lèi)型值的時(shí)候只需要對(duì)其中的字段f和dataLen進(jìn)行初始化。這是因?yàn)閣offset字段和roffset字段的零值都是0,而在未進(jìn)行過(guò)寫(xiě)操作和讀操作的時(shí)候它們的值理應(yīng)如此。對(duì)于字段fmutex、wmutex和rmutex來(lái)說(shuō),它們的零值即為可用的鎖。所以我們也不必對(duì)它們進(jìn)行顯式的初始化。

把變量df的值作為NewDataFile函數(shù)的第一個(gè)結(jié)果值體現(xiàn)了我們的設(shè)計(jì)意圖。但要想使*myDataFile類(lèi)型真正成為DataFile類(lèi)型的一個(gè)實(shí)現(xiàn)類(lèi)型,我們還需要為*myDataFile類(lèi)型編寫(xiě)出已在DataFile接口類(lèi)型中聲明的所有方法。其中最重要的當(dāng)屬Read方法和Write方法。

我們先來(lái)編寫(xiě)*myDataFile類(lèi)型的Read方法。該方法應(yīng)該按照如下步驟實(shí)現(xiàn)。

(1) 獲取并更新讀偏移量。

(2) 根據(jù)讀偏移量從文件中讀取一塊數(shù)據(jù)。

(3) 把該數(shù)據(jù)塊封裝成一個(gè)Data類(lèi)型值并將其作為結(jié)果值返回。

其中,前一個(gè)步驟在被執(zhí)行的時(shí)候應(yīng)該由互斥鎖rmutex保護(hù)起來(lái)。因?yàn)椋覀円蠖鄠€(gè)讀操作不能讀取同一個(gè)數(shù)據(jù)塊,并且它們應(yīng)該按順序的讀取文件中的數(shù)據(jù)塊。而第二個(gè)步驟,我們也會(huì)用讀寫(xiě)鎖fmutex加以保護(hù)。下面是這個(gè)Read方法的第一個(gè)版本:

復(fù)制代碼 代碼如下:


func (df *myDataFile) Read() (rsn int64, d Data, err error) {

 

// 讀取并更新讀偏移量

var offset int64

df.rmutex.Lock()

offset = df.roffset

df.roffset += int64(df.dataLen)

df.rmutex.Unlock()

 

//讀取一個(gè)數(shù)據(jù)塊

rsn = offset / int64(df.dataLen)

df.fmutex.RLock()

defer df.fmutex.RUnlock()

bytes := make([]byte, df.dataLen)

_, err = df.f.ReadAt(bytes, offset)

if err != nil {

return

}

d = bytes

return

}

 

可以看到,在讀取并更新讀偏移量的時(shí)候,我們用到了rmutex字段。這保證了可能同時(shí)運(yùn)行在多個(gè)Goroutine中的這兩行代碼:

復(fù)制代碼 代碼如下:


offset = df.roffset

 

df.roffset += int64(df.dataLen)

 

的執(zhí)行是互斥的。這是我們?yōu)榱双@取到不重復(fù)且正確的讀偏移量所必需采取的措施。

另一方面,在讀取一個(gè)數(shù)據(jù)塊的時(shí)候,我們適時(shí)的進(jìn)行了fmutex字段的讀鎖定和讀解鎖操作。fmutex字段的這兩個(gè)操作可以保證我們?cè)谶@里讀取到的是完整的數(shù)據(jù)塊。不過(guò),這個(gè)完整的數(shù)據(jù)塊卻并不一定是正確的。為什么會(huì)這樣說(shuō)呢?

請(qǐng)想象這樣一個(gè)場(chǎng)景。在我們的程序中,有3個(gè)Goroutine來(lái)并發(fā)的執(zhí)行某個(gè)*myDataFile類(lèi)型值的Read方法,并有2個(gè)Goroutine來(lái)并發(fā)的執(zhí)行該值的Write方法。通過(guò)前3個(gè)Goroutine的運(yùn)行,數(shù)據(jù)文件中的數(shù)據(jù)塊被依次的讀取了出來(lái)。但是,由于進(jìn)行寫(xiě)操作的Goroutine比進(jìn)行讀操作的Goroutine少,所以過(guò)不了多久讀偏移量roffset的值就會(huì)等于甚至大于寫(xiě)偏移量woffset的值。也就是說(shuō),讀操作很快就會(huì)沒(méi)有數(shù)據(jù)可讀了。這種情況會(huì)使上面的df.f.ReadAt方法返回的第二個(gè)結(jié)果值為代表錯(cuò)誤的非nil且會(huì)與io.EOF相等的值。實(shí)際上,我們不應(yīng)該把這樣的值看成錯(cuò)誤的代表,而應(yīng)該把它看成一種邊界情況。但不幸的是,我們?cè)谶@個(gè)版本的Read方法中并沒(méi)有對(duì)這種邊界情況做出正確的處理。該方法在遇到這種情況時(shí)會(huì)直接把錯(cuò)誤值返回給它的調(diào)用方。該調(diào)用方會(huì)得到讀取出錯(cuò)的數(shù)據(jù)塊的序列號(hào),但卻無(wú)法再次嘗試讀取這個(gè)數(shù)據(jù)塊。由于其他正在或后續(xù)執(zhí)行的Read方法會(huì)繼續(xù)增加讀偏移量roffset的值,所以當(dāng)該調(diào)用方再次調(diào)用這個(gè)Read方法的時(shí)候只可能讀取到在此數(shù)據(jù)塊后面的其他數(shù)據(jù)塊。注意,執(zhí)行Read方法時(shí)遇到上述情況的次數(shù)越多,被漏讀的數(shù)據(jù)塊也就會(huì)越多。為了解決這個(gè)問(wèn)題,我們編寫(xiě)了Read方法的第二個(gè)版本:

復(fù)制代碼 代碼如下:


func (df *myDataFile) Read() (rsn int64, d Data, err error) {

 

// 讀取并更新讀偏移量

// 省略若干條語(yǔ)句

//讀取一個(gè)數(shù)據(jù)塊

rsn = offset / int64(df.dataLen)

bytes := make([]byte, df.dataLen)

for {

df.fmutex.RLock()

_, err = df.f.ReadAt(bytes, offset)

if err != nil {

if err == io.EOF {

df.fmutex.RUnlock()

continue

}

df.fmutex.RUnlock()

return

}

d = bytes

df.fmutex.RUnlock()

return

}

}

 

在上面的Read方法展示中,我們省略了若干條語(yǔ)句。原因在這個(gè)位置上的那些語(yǔ)句并沒(méi)有任何變化。為了進(jìn)一步節(jié)省篇幅,我們?cè)诤竺嬉矔?huì)遵循這樣的省略原則。

第二個(gè)版本的Read方法使用for語(yǔ)句是為了達(dá)到這樣一個(gè)目的:在其中的df.f.ReadAt方法返回io.EOF錯(cuò)誤的時(shí)候繼續(xù)嘗試獲取同一個(gè)數(shù)據(jù)塊,直到獲取成功為止。注意,如果在該for代碼塊被執(zhí)行期間一直讓讀寫(xiě)鎖fmutex處于讀鎖定狀態(tài),那么針對(duì)它的寫(xiě)鎖定操作將永遠(yuǎn)不會(huì)成功,且相應(yīng)的Goroutine也會(huì)被一直阻塞。因?yàn)樗鼈兪腔コ獾摹K裕覀儾坏貌辉谠揻or語(yǔ)句塊中的每條return語(yǔ)句和continue語(yǔ)句的前面都加入一個(gè)針對(duì)該讀寫(xiě)鎖的讀解鎖操作,并在每次迭代開(kāi)始時(shí)都對(duì)fmutex進(jìn)行一次讀鎖定。顯然,這樣的代碼看起來(lái)很丑陋。冗余的代碼會(huì)使代碼的維護(hù)成本和出錯(cuò)幾率大大增加。并且,當(dāng)for代碼塊中的代碼引發(fā)了運(yùn)行時(shí)恐慌的時(shí)候,我們是很難及時(shí)的對(duì)讀寫(xiě)鎖fmutex進(jìn)行讀解鎖的。即便可以這樣做,那也會(huì)使Read方法的實(shí)現(xiàn)更加丑陋。我們因?yàn)橐幚硪环N邊界情況而去掉了defer df.fmutex.RUnlock()語(yǔ)句。這種做法利弊參半。

其實(shí),我們可以做得更好。但是這涉及到了其他同步工具。因此,我們以后再來(lái)對(duì)Read方法進(jìn)行進(jìn)一步的改造。順便提一句,當(dāng)df.f.ReadAt方法返回一個(gè)非nil且不等于io.EOF的錯(cuò)誤值的時(shí)候,我們總是應(yīng)該放棄再次獲取目標(biāo)數(shù)據(jù)塊的嘗試而立即將該錯(cuò)誤值返回給Read方法的調(diào)用方。因?yàn)檫@樣的錯(cuò)誤很可能是嚴(yán)重的(比如,f字段代表的文件被刪除了),需要交由上層程序去處理。

現(xiàn)在,我們來(lái)考慮*myDataFile類(lèi)型的Write方法。與Read方法相比,Write方法的實(shí)現(xiàn)會(huì)簡(jiǎn)單一些。因?yàn)楹笳卟粫?huì)涉及到邊界情況。在該方法中,我們需要進(jìn)行兩個(gè)步驟,即:獲取并更新寫(xiě)偏移量和向文件寫(xiě)入一個(gè)數(shù)據(jù)塊。我們直接給出Write方法的實(shí)現(xiàn):

復(fù)制代碼 代碼如下:


func (df *myDataFile) Write(d Data) (wsn int64, err error) {

 

// 讀取并更新寫(xiě)偏移量

var offset int64

df.wmutex.Lock()

offset = df.woffset

df.woffset += int64(df.dataLen)

df.wmutex.Unlock()

 

//寫(xiě)入一個(gè)數(shù)據(jù)塊

wsn = offset / int64(df.dataLen)

var bytes []byte

if len(d) > int(df.dataLen) {

bytes = d[0:df.dataLen]

} else {

bytes = d

}

df.fmutex.Lock()

df.fmutex.Unlock()

_, err = df.f.Write(bytes)

return

}

 

這里需要注意的是,當(dāng)參數(shù)d的值的長(zhǎng)度大于數(shù)據(jù)塊的最大長(zhǎng)度的時(shí)候,我們會(huì)先進(jìn)行截短處理再將數(shù)據(jù)寫(xiě)入文件。如果沒(méi)有這個(gè)截短處理,我們?cè)诤竺嬗?jì)算的已讀數(shù)據(jù)塊的序列號(hào)和已寫(xiě)數(shù)據(jù)塊的序列號(hào)就會(huì)不正確。

有了編寫(xiě)前面兩個(gè)方法的經(jīng)驗(yàn),我們可以很容易的編寫(xiě)出*myDataFile類(lèi)型的Rsn方法和Wsn方法:

復(fù)制代碼 代碼如下:


func (df *myDataFile) Rsn() int64 {

 

df.rmutex.Lock()

defer df.rmutex.Unlock()

return df.roffset / int64(df.dataLen)

}

func (df *myDataFile) Wsn() int64 {

df.wmutex.Lock()

defer df.wmutex.Unlock()

return df.woffset / int64(df.dataLen)

}

 

這兩個(gè)方法的實(shí)現(xiàn)分別涉及到了對(duì)互斥鎖rmutex和wmutex的鎖定操作。同時(shí),我們也通過(guò)使用defer語(yǔ)句保證了對(duì)它們的及時(shí)解鎖。在這里,我們對(duì)已讀數(shù)據(jù)塊的序列號(hào)rsn和已寫(xiě)數(shù)據(jù)塊的序列號(hào)wsn的計(jì)算方法與前面示例中的方法是相同的。它們都是用相關(guān)的偏移量除以數(shù)據(jù)塊長(zhǎng)度后得到的商來(lái)作為相應(yīng)的序列號(hào)(或者說(shuō)計(jì)數(shù))的值。

至于*myDataFile類(lèi)型的DataLen方法的實(shí)現(xiàn),我們無(wú)需呈現(xiàn)。它只是簡(jiǎn)單地將dataLen字段的值作為其結(jié)果值返回而已。

編寫(xiě)上面這個(gè)完整示例的主要目的是展示互斥鎖和讀寫(xiě)鎖在實(shí)際場(chǎng)景中的應(yīng)用。由于還沒(méi)有講到Go語(yǔ)言提供的其他同步工具,所以我們?cè)谙嚓P(guān)方法中所有需要同步的地方都是用鎖來(lái)實(shí)現(xiàn)的。然而,其中的一些問(wèn)題用鎖來(lái)解決是不足夠或不合適的。我們會(huì)在本節(jié)的后續(xù)部分中逐步的對(duì)它們進(jìn)行改進(jìn)。

從這兩種鎖的源碼中可以看出,它們是同源的。讀寫(xiě)鎖的內(nèi)部是用互斥鎖來(lái)實(shí)現(xiàn)寫(xiě)鎖定操作之間的互斥的。我們可以把讀寫(xiě)鎖看做是互斥鎖的一種擴(kuò)展。除此之外,這兩種鎖實(shí)現(xiàn)在內(nèi)部都用到了操作系統(tǒng)提供的同步工具——信號(hào)燈。互斥鎖內(nèi)部使用一個(gè)二值信號(hào)燈(只有兩個(gè)可能的值的信號(hào)燈)來(lái)實(shí)現(xiàn)鎖定操作之間的互斥,而讀寫(xiě)鎖內(nèi)部則使用一個(gè)二值信號(hào)燈和一個(gè)多值信號(hào)燈(可以有多個(gè)可能的值的信號(hào)燈)來(lái)實(shí)現(xiàn)寫(xiě)鎖定操作與讀鎖定操作之間的互斥。當(dāng)然,為了進(jìn)行精確的協(xié)調(diào),它們還使用到了其他一些字段和變量。由于篇幅原因,我們就不在這里贅述了。如果讀者對(duì)此感興趣的話,可以去閱讀sync代碼包中的相關(guān)源碼文件。

延伸 · 閱讀

精彩推薦
  • GolangGolang中Bit數(shù)組的實(shí)現(xiàn)方式

    Golang中Bit數(shù)組的實(shí)現(xiàn)方式

    這篇文章主要介紹了Golang中Bit數(shù)組的實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧...

    天易獨(dú)尊11682021-06-09
  • Golanggolang的httpserver優(yōu)雅重啟方法詳解

    golang的httpserver優(yōu)雅重啟方法詳解

    這篇文章主要給大家介紹了關(guān)于golang的httpserver優(yōu)雅重啟的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,...

    helight2992020-05-14
  • Golanggo日志系統(tǒng)logrus顯示文件和行號(hào)的操作

    go日志系統(tǒng)logrus顯示文件和行號(hào)的操作

    這篇文章主要介紹了go日志系統(tǒng)logrus顯示文件和行號(hào)的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧...

    SmallQinYan12302021-02-02
  • GolangGolang通脈之?dāng)?shù)據(jù)類(lèi)型詳情

    Golang通脈之?dāng)?shù)據(jù)類(lèi)型詳情

    這篇文章主要介紹了Golang通脈之?dāng)?shù)據(jù)類(lèi)型,在編程語(yǔ)言中標(biāo)識(shí)符就是定義的具有某種意義的詞,比如變量名、常量名、函數(shù)名等等,Go語(yǔ)言中標(biāo)識(shí)符允許由...

    4272021-11-24
  • Golanggolang json.Marshal 特殊html字符被轉(zhuǎn)義的解決方法

    golang json.Marshal 特殊html字符被轉(zhuǎn)義的解決方法

    今天小編就為大家分享一篇golang json.Marshal 特殊html字符被轉(zhuǎn)義的解決方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧 ...

    李浩的life12792020-05-27
  • Golanggolang 通過(guò)ssh代理連接mysql的操作

    golang 通過(guò)ssh代理連接mysql的操作

    這篇文章主要介紹了golang 通過(guò)ssh代理連接mysql的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧...

    a165861639710342021-03-08
  • Golanggo語(yǔ)言制作端口掃描器

    go語(yǔ)言制作端口掃描器

    本文給大家分享的是使用go語(yǔ)言編寫(xiě)的TCP端口掃描器,可以選擇IP范圍,掃描的端口,以及多線程,有需要的小伙伴可以參考下。 ...

    腳本之家3642020-04-25
  • Golanggolang如何使用struct的tag屬性的詳細(xì)介紹

    golang如何使用struct的tag屬性的詳細(xì)介紹

    這篇文章主要介紹了golang如何使用struct的tag屬性的詳細(xì)介紹,從例子說(shuō)起,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看...

    Go語(yǔ)言中文網(wǎng)11352020-05-21
Weibo Article 1 Weibo Article 2 Weibo Article 3 Weibo Article 4 Weibo Article 5 Weibo Article 6 Weibo Article 7 Weibo Article 8 Weibo Article 9 Weibo Article 10 Weibo Article 11 Weibo Article 12 Weibo Article 13 Weibo Article 14 Weibo Article 15 Weibo Article 16 Weibo Article 17 Weibo Article 18 Weibo Article 19 Weibo Article 20 Weibo Article 21 Weibo Article 22 Weibo Article 23 Weibo Article 24 Weibo Article 25 Weibo Article 26 Weibo Article 27 Weibo Article 28 Weibo Article 29 Weibo Article 30 Weibo Article 31 Weibo Article 32 Weibo Article 33 Weibo Article 34 Weibo Article 35 Weibo Article 36 Weibo Article 37 Weibo Article 38 Weibo Article 39 Weibo Article 40
主站蜘蛛池模板: 日韩欧美国产一区二区三区 | 日本一区二区三区精品视频 | 亚洲一区二区在线视频 | 午夜影院免费观看 | 亚洲婷婷综合网 | 亚洲国产精品视频 | 欧美电影一区 | 黄色国产一级片 | 在线观看成人小视频 | 国产欧美久久久久久 | 天天爽夜夜爽夜夜爽精品视频 | 级毛片| 中文字幕一区二区三 | 亚洲精品999 | 亚洲国产精品久久 | 欧美成人免费视频 | 亚洲精品久久久久久动漫 | 亚洲欧美视频网站 | av免费在线观看网站 | 免费在线国产 | 高清视频一区二区三区 | 亚洲视频在线免费观看 | 欧美视频三区 | 日日操操| 亚洲免费成人在线视频 | 亚洲激情久久 | 欧美精品久久久 | 国产日韩中文字幕 | a国产精品 | 亚洲成人久久久 | 亚洲一区二区免费看 | 99久久精品免费看国产四区 | 免费在线黄色电影 | 精品久久久久久亚洲综合网 | 久久久久国产一区二区三区 | 日韩精品免费在线观看 | 久久噜噜噜精品国产亚洲综合 | 午夜天堂 | 秋霞精品 | 国产精品久久久 | 尤物视频在线观看 |