亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 編程 > Golang > 正文

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

2020-04-01 19:23:46
字體:
來源:轉載
供稿:網(wǎng)友
這篇文章主要介紹了GO語言并發(fā)編程之互斥鎖、讀寫鎖詳解,本文是GO并發(fā)編程實戰(zhàn)一書的樣章,詳細講解了互斥鎖、讀寫鎖,然后給出了一個完整示例,需要的朋友可以參考下
 

在本節(jié),我們對Go語言所提供的與鎖有關的API進行說明。這包括了互斥鎖和讀寫鎖。我們在第6章描述過互斥鎖,但卻沒有提到過讀寫鎖。這兩種鎖對于傳統(tǒng)的并發(fā)程序來說都是非常常用和重要的。

一、互斥鎖

互斥鎖是傳統(tǒng)的并發(fā)程序對共享資源進行訪問控制的主要手段。它由標準庫代碼包sync中的Mutex結構體類型代表。sync.Mutex類型(確切地說,是*sync.Mutex類型)只有兩個公開方法——Lock和Unlock。顧名思義,前者被用于鎖定當前的互斥量,而后者則被用來對當前的互斥量進行解鎖。

類型sync.Mutex的零值表示了未被鎖定的互斥量。也就是說,它是一個開箱即用的工具。我們只需對它進行簡單聲明就可以正常使用了,就像這樣:

 

復制代碼代碼如下:

var mutex sync.Mutex

 

mutex.Lock()

 

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

我們一般會在鎖定互斥鎖之后緊接著就用defer語句來保證該互斥鎖的及時解鎖。請看下面這個函數(shù):

復制代碼代碼如下:

var mutex sync.Mutex

 

func write() {

mutex.Lock()

defer mutex.Unlock()

// 省略若干條語句

}

 

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

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

 

復制代碼代碼如下:

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

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

 

復制代碼代碼如下:

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

實際上,我們之所以能夠通過使用互斥鎖對共享資源的唯一性訪問進行控制正是因為它的這一特性。這有效的對競態(tài)條件進行了消除。

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

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

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

二、讀寫鎖

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

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

在Go語言中,讀寫鎖由結構體類型sync.RWMutex代表。與互斥鎖類似,sync.RWMutex類型的零值就已經(jīng)是立即可用的讀寫鎖了。在此類型的方法集合中包含了兩對方法,即:

 

復制代碼代碼如下:

func (*RWMutex) Lock

 

func (*RWMutex) Unlock

 

 

復制代碼代碼如下:

func (*RWMutex) RLock

 

func (*RWMutex) RUnlock

 

前一對方法的名稱和簽名與互斥鎖的那兩個方法完全一致。它們分別代表了對寫操作的鎖定和解鎖。以下簡稱它們?yōu)閷戞i定和寫解鎖。而后一對方法則分別表示了對讀操作的鎖定和解鎖。以下簡稱它們?yōu)樽x鎖定和讀解鎖。

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

現(xiàn)在來關注寫解鎖和讀解鎖。如果對一個未被寫鎖定的讀寫鎖進行寫解鎖,那么會引發(fā)一個運行時恐慌。類似的,當對一個未被讀鎖定的讀寫鎖進行讀解鎖的時候也會引發(fā)一個運行時恐慌。寫解鎖在進行的同時會試圖喚醒所有因進行讀鎖定而被阻塞的Goroutine。而讀解鎖在進行的時候則會試圖喚醒一個因進行寫鎖定而被阻塞的Goroutine。

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

除了我們在前面詳細講解的那兩對方法之外,*sync.RWMutex類型還擁有另外一個方法——RLocker。這個RLocker方法會返回一個實現(xiàn)了sync.Locker接口的值。sync.Locker接口類型包含了兩個方法,即:Lock和Unlock。細心的讀者可能會發(fā)現(xiàn),*sync.Mutex類型和*sync.RWMutex類型都是該接口類型的實現(xiàn)類型。實際上,我們在調(diào)用*sync.RWMutex類型值的RLocker方法之后所得到的結果值就是這個值本身。只不過,這個結果值的Lock方法和Unlock方法分別對應了針對該讀寫鎖的讀鎖定操作和讀解鎖操作。換句話說,我們在對一個讀寫鎖的RLocker方法的結果值的Lock方法或Unlock方法進行調(diào)用的時候實際上是在調(diào)用該讀寫鎖的RLock方法或RUnlock方法。這樣的操作適配在實現(xiàn)上并不困難。我們自己也可以很容易的編寫出這些方法的實現(xiàn)。通過讀寫鎖的RLocker方法獲得這樣一個結果值的實際意義在于,我們可以在之后以相同的方式對該讀寫鎖中的“寫鎖”和“讀鎖”進行操作。這為相關操作的靈活適配和替換提供了方便。

三、鎖的完整示例

我們下面來看一個與上述鎖實現(xiàn)有關的示例。在Go語言的標準庫代碼包os中有一個名為File的結構體類型。os.File類型的值可以被用來代表文件系統(tǒng)中的某一個文件或目錄。它的方法集合中包含了很多方法,其中的一些方法被用來對相應的文件進行寫操作和讀操作。

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

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

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

為了實現(xiàn)上述需求,我們需要創(chuàng)建一個類型。作為該類型的行為定義,我們先編寫了一個這樣的接口:

復制代碼代碼如下:

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

 

type DataFile interface {

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

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

// 寫入一個數(shù)據(jù)塊。

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

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

Rsn() int64

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

Wsn() int64

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

DataLen() uint32

}

 

其中,類型Data被聲明為一個[]byte的別名類型:

 

復制代碼代碼如下:

// 數(shù)據(jù)的類型

 

type Data []byte

 

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

根據(jù)上面對需求的簡單分析和這個DataFile接口類型聲明,我們就可以來編寫真正的實現(xiàn)了。我們將這個實現(xiàn)類型命名為myDataFile。它的基本結構如下:

復制代碼代碼如下:

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

 

type myDataFile struct {

f       *os.File     // 文件。

fmutex sync.RWMutex // 被用于文件的讀寫鎖。

woffset int64       // 寫操作需要用到的偏移量。

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

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

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

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

}

 

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

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

復制代碼代碼如下:

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

}

 

可以看到,我們在創(chuàng)建*myDataFile類型值的時候只需要對其中的字段f和dataLen進行初始化。這是因為woffset字段和roffset字段的零值都是0,而在未進行過寫操作和讀操作的時候它們的值理應如此。對于字段fmutex、wmutex和rmutex來說,它們的零值即為可用的鎖。所以我們也不必對它們進行顯式的初始化。

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

我們先來編寫*myDataFile類型的Read方法。該方法應該按照如下步驟實現(xiàn)。

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

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

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

其中,前一個步驟在被執(zhí)行的時候應該由互斥鎖rmutex保護起來。因為,我們要求多個讀操作不能讀取同一個數(shù)據(jù)塊,并且它們應該按順序的讀取文件中的數(shù)據(jù)塊。而第二個步驟,我們也會用讀寫鎖fmutex加以保護。下面是這個Read方法的第一個版本:

復制代碼代碼如下:

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()

 

//讀取一個數(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

}

 

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

復制代碼代碼如下:

offset = df.roffset

 

df.roffset += int64(df.dataLen)

 

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

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

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

復制代碼代碼如下:

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

 

// 讀取并更新讀偏移量

// 省略若干條語句

//讀取一個數(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方法展示中,我們省略了若干條語句。原因在這個位置上的那些語句并沒有任何變化。為了進一步節(jié)省篇幅,我們在后面也會遵循這樣的省略原則。

第二個版本的Read方法使用for語句是為了達到這樣一個目的:在其中的df.f.ReadAt方法返回io.EOF錯誤的時候繼續(xù)嘗試獲取同一個數(shù)據(jù)塊,直到獲取成功為止。注意,如果在該for代碼塊被執(zhí)行期間一直讓讀寫鎖fmutex處于讀鎖定狀態(tài),那么針對它的寫鎖定操作將永遠不會成功,且相應的Goroutine也會被一直阻塞。因為它們是互斥的。所以,我們不得不在該for語句塊中的每條return語句和continue語句的前面都加入一個針對該讀寫鎖的讀解鎖操作,并在每次迭代開始時都對fmutex進行一次讀鎖定。顯然,這樣的代碼看起來很丑陋。冗余的代碼會使代碼的維護成本和出錯幾率大大增加。并且,當for代碼塊中的代碼引發(fā)了運行時恐慌的時候,我們是很難及時的對讀寫鎖fmutex進行讀解鎖的。即便可以這樣做,那也會使Read方法的實現(xiàn)更加丑陋。我們因為要處理一種邊界情況而去掉了defer df.fmutex.RUnlock()語句。這種做法利弊參半。

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

現(xiàn)在,我們來考慮*myDataFile類型的Write方法。與Read方法相比,Write方法的實現(xiàn)會簡單一些。因為后者不會涉及到邊界情況。在該方法中,我們需要進行兩個步驟,即:獲取并更新寫偏移量和向文件寫入一個數(shù)據(jù)塊。我們直接給出Write方法的實現(xiàn):

復制代碼代碼如下:

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

 

// 讀取并更新寫偏移量

var offset int64

df.wmutex.Lock()

offset = df.woffset

df.woffset += int64(df.dataLen)

df.wmutex.Unlock()

 

//寫入一個數(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

}

 

這里需要注意的是,當參數(shù)d的值的長度大于數(shù)據(jù)塊的最大長度的時候,我們會先進行截短處理再將數(shù)據(jù)寫入文件。如果沒有這個截短處理,我們在后面計算的已讀數(shù)據(jù)塊的序列號和已寫數(shù)據(jù)塊的序列號就會不正確。

有了編寫前面兩個方法的經(jīng)驗,我們可以很容易的編寫出*myDataFile類型的Rsn方法和Wsn方法:

復制代碼代碼如下:

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)

}

 

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

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

編寫上面這個完整示例的主要目的是展示互斥鎖和讀寫鎖在實際場景中的應用。由于還沒有講到Go語言提供的其他同步工具,所以我們在相關方法中所有需要同步的地方都是用鎖來實現(xiàn)的。然而,其中的一些問題用鎖來解決是不足夠或不合適的。我們會在本節(jié)的后續(xù)部分中逐步的對它們進行改進。

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


發(fā)表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發(fā)表
国产精品美女网站| 极品粉嫩小仙女高潮喷水久久| 欧美日韩一区在线| 亚洲综合精品一区二区| 一区二区三区视频免费视频观看网站| free性欧美hd另类精品| av资源站久久亚洲| 羞羞色午夜精品一区二区三区| 8x8x成人免费视频| 粉嫩av免费一区二区三区| 97视频热人人精品免费| 免费看黄色网| 欧美高清你懂得| 91精品视频免费观看| 国产福利电影在线观看| 色老头在线观看| 性一交一乱一伧国产女士spa| 亚洲怡红院在线观看| 99久久精品免费看| 美女网站色免费| 巨大黑人极品videos精品| 国产偷窥女洗浴在线观看亚洲| 欧美电影免费观看高清| 福利一区视频在线观看| 无码少妇一区二区三区| 性色国产成人久久久精品| 国产三级按摩推拿按摩| 日本黄色免费| 欧美6699在线视频免费| 波多野一区二区| av在线播放国产| 亚洲综合激情网| 在线精品国产欧美| 亚洲少妇屁股交4| 国产精品国产亚洲精品| 成人免费淫片免费观看| 色婷婷狠狠综合| 99热超碰在线| 成人午夜精品久久久久久久蜜臀| 国产精品视频自拍| 欧美 日韩 国产一区二区在线视频| 精品国产一区二区三区四区在线观看| 羞羞视频网页| 亚洲一区二区激情| 国产成人亚洲精品自产在线| 国产成人禁片在线观看| 日韩精品在线观看免费| 卡一卡2卡三精品| 欧美黑人一级片| 欧美自拍偷拍一区二区| 91免费版网站在线观看| 欧美一区二区视频在线| 一个人看的日本www的免费视频| 欧美综合激情| 天天综合天天做天天综合| 精品无码av无码免费专区| 欧美福利电影在线观看| 三级毛片电影网站| 日本黄色片一级片| 久久免费高清| 57pao成人国产永久免费| 国产一级免费看| 国产一区二区日韩| 韩国三级在线播放| 欧美v亚洲v综合v国产v仙踪林| 欧美激情亚洲色图| 日本不卡一二区| 欧美一区二区私人影院日本| 亚洲电影欧美电影有声小说| 3d动漫精品啪啪1区2区免费| 亚洲视频一区二区免费在线观看| 污视频在线播放| 国产欧美一区二区三区久久人妖| 久久99精品国产.久久久久久| 亚洲午夜性刺激影院| 日韩精品一区二区三区三区免费| 欧美色18zzzzxxxxx| 中文字幕在线2018| 国产va免费精品高清在线观看| 国产成人自拍网| 国产精品福利在线观看播放| 蜜桃视频在线入口www| 国产激情在线播放| a毛片在线观看| 成人欧美一区二区三区黑人麻豆| 美女一区二区三区在线观看| 久久久成人av毛片免费观看| 精品亚洲美女网站| 亚洲激情av在线| 91社区视频在线观看| 日韩三级成人av网| 色婷婷综合五月| 国产成人免费av电影| 国产破处视频在线观看| 永久免费黄色片| 黄色片视频免费| 日本免费视频在线观看| 久久久久久亚洲精品美女| 欧美日韩亚洲激情| 中文字幕天堂网| 久久久久久久久久久国产精品| 亚洲乱亚洲乱妇无码| 91性高潮久久久久久久| 欧美日韩中文字幕在线播放| 久久一区二区中文字幕| 成人午夜两性视频| www.超碰97| 久久精品国产欧美亚洲人人爽| 高清国语自产拍免费一区二区三区| 亚洲一区二区视频在线观看| 偷拍自拍在线看| 亚洲伦片免费看| 中文字幕有码视频| 日韩一区电影| 中文字幕精品综合| 色哟哟欧美精品| 欧美成人一区二免费视频软件| 91成人精品网站| 一区二区三区视频免费在线观看| 亚洲日韩中文字幕一区| 亚洲精品四区| 91在线精品一区二区三区| 亚洲欧美成人| 97在线免费视频观看| 国产精选一区二区三区不卡催乳| 久久亚洲精品中文字幕蜜潮电影| 最新精品视频在线| 免费日韩视频| 内衣办公室在线| 日本少妇毛茸茸| hd100%videos日本| 大片免费在线看视频| www.久久成人| 霍思燕三级露全乳照| jiujiure精品视频播放| 亚洲日本在线播放| 日本高清不卡视频| 99亚洲伊人久久精品影院红桃| 成人在线观看网址| 亚洲欧美在线aaa| 99免费精品在线观看| 性色av蜜臀av| 老司机凹凸av亚洲导航| 色婷婷av一区二区三区丝袜美腿| 国产一区二区黑人欧美xxxx| 国产日韩1区| 91麻豆视频在线观看| 精品视频免费看| 欧美成人免费网站| 亚洲国产精品免费| 欧美怡春院一区二区三区| 欧美精品密入口播放| 国产午夜久久久久| 久久中文字幕免费| 国产丶欧美丶日本不卡视频| 精品国模一区二区三区欧美| 日韩精品黄色| 精品国产福利在线| 少妇荡乳情欲办公室456视频| 中文字幕在线影院| 欧美人xxxxx| 国产露出视频在线观看| 精品久久久久久一区二区里番| 国产三级做爰在线观看| 欧美一级特黄aaa| 一区二区三区在线播放视频| 欧洲精品一区二区| 亚洲自拍第三页| а√天堂中文在线资源8| 久久无码专区国产精品s| 日本熟妇色xxxxx日本免费看| av中文字幕电影在线看| www.久久99| 91xxxxx| 国产精品久久久久久久app| 日韩成人在线电影网| 精品黄色免费中文电影在线播放| 在线免费av资源| 欧美日本一区二区三区四区| 午夜cr在线观看高清在线视频完整版| 天干夜天天夜天干天ww| 日韩精品视频免费| 香蕉久久精品日日躁夜夜躁| 伊人影院在线观看视频| 美女伦理水蜜桃4| 色哟哟亚洲精品| 欧美精品一区二区三区免费播放| 人妻在线日韩免费视频| 欧美破处大片在线视频| 色欲人妻综合网| 成人激情av| 成人欧美一区二区| 日韩欧美第二区在线观看| 欧美日韩国产欧| 国产精品中文久久久久久久| 欧美肥臀大乳一区二区免费视频| 国内一区二区三区在线视频| 久久久久久久一区二区| 亚洲free性xxxx护士hd| 精品一区二区国语对白| 国内外成人免费激情在线视频网站| 女同性恋一区二区三区| 日本高清视频一区| 一色道久久88加勒比一| 国产精品九九九九九| 国产一区二区三区视频| 欧美色图17p| 日韩视频在线你懂得| 午夜国产福利| 国产精品一区二区人妻喷水| 男人天堂新地址| 电影网一区二区| 国产精品网站一区| 在线电影欧美日韩一区二区私密| 91精品亚洲一区在线观看| 久草在线资源网站| 国内自拍视频在线看免费观看| 欧美日韩国产a| 国内精品卡一卡二卡三新区| 国产精品一区二区三区乱码| 日本亚洲一区二区| 成人综合婷婷国产精品久久免费| 久久精品国产亚洲av麻豆| 成人美女av在线直播| 91在线资源| 日韩成人一级大片| 岛国视频午夜一区免费在线观看| 成人在线免费视频观看| 国产精选在线观看| 精品国产亚洲一区二区三区在线| 精品乱码一区二区三区| 精品自拍视频| 日韩av影院在线观看| 久久久精品视频在线| 成全电影大全在线观看| 狠狠色伊人亚洲综合成人| 少妇久久久久久被弄到高潮| 91九色国产社区在线观看| 一区二区三区国产精华| 91av在线免费播放| 91精品国产综合久久久久久豆腐| 久久久久亚洲精品国产| 菠萝菠萝蜜网站| 夜夜躁狠狠躁日日躁av| 欧美精品情趣视频| 欧美日韩免费高清一区色橹橹| 欧美一级淫片007| 久久福利精品| 日本久久中文字幕| 不卡影院免费观看| 91九色丨porny丨极品女神| 男人天堂视频在线观看| 欧美日韩精品免费观看视完整| 超碰人人人人人人人| 日本中文字幕中出在线| 亚洲视频小说图片| 精品视频一区二区三区在线观看| 精品亚洲成a人在线观看| 欧美日韩在线高清| 久久久精品免费网站| 久久伊人色综合| 99久精品视频在线观看视频| 日产精品高清视频免费| 麻豆影视在线观看| 午夜精品福利一区二区蜜股av| 亚洲视频中文| 亚洲在线一区二区三区| 国产亚洲欧洲黄色| 在线视频不卡国产| 久久国产精品影视| 伊人久久大香线蕉精品组织观看| 综合久久国产九一剧情麻豆| 成人深夜视频在线观看| 欧美性xxxx69| 热舞福利精品大尺度视频| 婷婷激情四射五月天| 久久电影国产免费久久电影| 波多野结衣综合网| 人人妻人人澡人人爽精品欧美一区| 色橹橹高清视频在线播放| 日本不卡在线观看视频| 作爱视频免费观看视频在线播放激情网| 在线看的av网站| 亚洲欧洲中文日韩久久av乱码| www.久久久久久.com| 久久久久久**毛片大全| 午夜精品在线播放| 懂色av粉嫩av蜜乳av| www.91在线| 在线观看a级片| 国产精品人人妻人人爽| 一区二区三区麻豆| 国产盗摄精品一区二区三区在线| 精品久久97| 五月精品视频| 免费看成人a| 成人免费在线视频观看| 欧美精品无码一区二区三区| 亚洲欧美国产高清va在线播放| 91国内精品久久久| 日韩精品免费播放| 久久五月天综合| 久久久久久久久久一区二区三区| 99免费看香蕉视频| 菁菁伊人国产精品| 亚洲片在线观看| 亚洲精品视频一二三| 色诱视频网站一区| 青青草成人免费视频| 国产精品av在线播放| 久久精品久久精品久久| 久久艳妇乳肉豪妇荡乳av| 国产网站免费观看| 日韩在线观看高清| 久久99性xxx老妇胖精品| 91sa在线看| 99re6这里只有精品| 中国黄色a级片| 国产精欧美一区二区三区蓝颜男同| 欧美日韩国产一区在线| 亚洲色图18p| 99久久夜色精品国产网站| 欧美乱做爰xxxⅹ久久久| 亚洲精品电影久久久| 人人狠狠综合久久亚洲| 国产在线视频欧美一区| 精品日韩一区| 国产精品久久久久一区二区|