golang:1.併發編程之互斥鎖、讀寫鎖詳解

本文轉載自junjie,然後稍做修改。編程

1、互斥鎖安全

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

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

代碼以下:
var mutex sync.Mutex

mutex.Lock()函數

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

      咱們通常會在鎖定互斥鎖以後緊接着就用defer語句來保證該互斥鎖的及時解鎖。請看下面這個函數:性能

代碼以下:
var mutex sync.Mutex

func write() {ui

mutex.Lock()spa

defer mutex.Unlock()操作系統

// 省略若干條語句

}

函數write中的這條defer語句保證了在該函數被執行結束以前互斥鎖mutex必定會被解鎖。這省去了咱們在全部return語句以前以及異常發生之時重複的附加解鎖操做的工做。在函數的內部執行流程相對複雜的狀況下,這個工做量是不容忽視的,而且極易出現遺漏和致使錯誤。因此,這裏的defer語句老是必要的。在Go語言中,這是很重要的一個慣用法。咱們應該養成這種良好的習慣。

      對於同一個互斥鎖的鎖定操做和解鎖操做老是應該成對的出現。若是咱們鎖定了一個已被鎖定的互斥鎖,那麼進行重複鎖定操做的Goroutine將會被阻塞,直到該互斥鎖回到解鎖狀態。請看下面的示例:

代碼以下:

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)

}

咱們把執行repeatedlyLock函數的Goroutine稱爲G0。而在repeatedlyLock函數中,咱們又啓用了3個Goroutine,並分別把它們命名爲G一、G2和G3。能夠看到,咱們在啓用這3個Goroutine以前就已經對互斥鎖mutex進行了鎖定,而且在這3個Goroutine將要執行的go函數的開始處也加入了對mutex的鎖定操做。這樣作的意義是模擬併發地對同一個互斥鎖進行鎖定的情形。當for語句被執行完畢以後,咱們先讓G0小睡1秒鐘,以使運行時系統有充足的時間開始運行G一、G2和G3。在這以後,解鎖mutex。爲了可以讓讀者更加清晰地瞭解到repeatedlyLock函數被執行的狀況,咱們在這些鎖定和解鎖操做的先後加入了若干條打印語句,並在打印內容中添加了咱們爲這幾個Goroutine起的名字。也因爲這個緣由,咱們在repeatedlyLock函數的最後再次編寫了一條「睡眠」語句,以此爲可能出現的其餘打印內容再等待一小會兒。

     通過短暫的執行,標準輸出上會出現以下內容:

代碼以下:
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)

從這八行打印內容中,咱們能夠清楚的看出上述四個Goroutine的執行狀況。首先,在repeatedlyLock函數被執行伊始,對互斥鎖的第一次鎖定操做便被進行並順利地完成。這由第一行和第二行打印內容能夠看出。然後,在repeatedlyLock函數中被啓用的那三個Goroutine在G0的第一次「睡眠」期間開始被運行。當相應的go函數中的對互斥鎖的鎖定操做被進行的時候,它們都被阻塞住了。緣由是該互斥鎖已處於鎖定狀態了。這就是咱們在這裏只看到了三個連續的Lock the lock. (G<i>)而沒有當即看到The lock is locked. (G<i>)的緣由。隨後,G0「睡醒」並解鎖互斥鎖。這使得正在被阻塞的G一、G2和G3都會有機會從新鎖定該互斥鎖。可是,只有一個Goroutine會成功。成功完成鎖定操做的某一個Goroutine會繼續執行在該操做以後的語句。而其餘Goroutine將繼續被阻塞,直到有新的機會到來。這也就是上述打印內容中的最後三行所表達的含義。顯然,G1搶到了此次機會併成功鎖定了那個互斥鎖。

      實際上,咱們之因此可以經過使用互斥鎖對共享資源的惟一性訪問進行控制正是由於它的這一特性。這有效的對競態條件進行了消除。

      互斥鎖的鎖定操做的逆操做並不會引發任何Goroutine的阻塞。可是,它的進行有可能引起運行時恐慌。更確切的講,當咱們對一個已處於解鎖狀態的互斥鎖進行解鎖操做的時候,就會已發一個運行時恐慌。這種狀況極可能會出如今相對複雜的流程之中——咱們可能會在某個或多個分支中重複的加入針對同一個互斥鎖的解鎖操做。避免這種狀況發生的最簡單、有效的方式依然是使用defer語句。這樣更容易保證解鎖操做的惟一性。

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

      互斥鎖是咱們見到過的衆多同步工具中最簡單的一個。只要遵循前面說起的幾個小技巧,咱們就能夠以正確、高效的方式使用互斥鎖,並用它來確保對共享資源的訪問的惟一性。下面咱們來看看稍微複雜一些的鎖實現——讀寫鎖。

2、讀寫鎖

      讀寫鎖便是針對於讀寫操做的互斥鎖。它與普通的互斥鎖最大的不一樣就是,它能夠分別針對讀操做和寫操做進行鎖定和解鎖操做。讀寫鎖遵循的訪問控制規則與互斥鎖有所不一樣。在讀寫鎖管轄的範圍內,它容許任意個讀操做的同時進行。可是,在同一時刻,它只容許有一個寫操做在進行。而且,在某一個寫操做被進行的過程當中,讀操做的進行也是不被容許的。也就是說,讀寫鎖控制下的多個寫操做之間都是互斥的,而且寫操做與讀操做之間也都是互斥的。可是,多個讀操做之間卻不存在互斥關係。

      這樣的規則對於針對同一塊數據的併發讀寫來說是很是貼切的。由於,不管讀操做的併發量有多少,這些操做都不會對數據自己形成變動。而寫操做不但會對同時進行的其餘寫操做進行干擾,還有可能形成同時進行的讀操做的結果的不正確。例如,在32位的操做系統中,針對int64類型值的讀操做和寫操做都不可能只由一個CPU指令完成。在一個寫操做被進行的過程中,針對同一個只的讀操做可能會讀取到未被修改完成的值。該值既不與舊的值相等,也不等於新的值。這種錯誤每每不易被發現,且很難被修正。所以,在這樣的場景下,讀寫鎖能夠在大大下降因使用鎖而對程序性能形成的損耗的狀況下完成對共享資源的訪問控制。

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

代碼以下:
func (*RWMutex) Lock

func (*RWMutex) Unlock

代碼以下:
func (*RWMutex) RLock

func (*RWMutex) RUnlock 

前一對方法的名稱和簽名與互斥鎖的那兩個方法徹底一致。它們分別表明了對寫操做的鎖定和解鎖。如下簡稱它們爲寫鎖定和寫解鎖。然後一對方法則分別表示了對讀操做的鎖定和解鎖。如下簡稱它們爲讀鎖定和讀解鎖。

      對已被寫鎖定的讀寫鎖進行寫鎖定,會形成當前Goroutine的阻塞,直到該讀寫鎖被寫解鎖。固然,若是有多個Goroutine所以而被阻塞,那麼當對應的寫解鎖被進行之時只會使其中一個Goroutine的運行被恢復。相似的,對一個已被寫鎖定的讀寫鎖進行讀鎖定,也會阻塞相應的Goroutine。但不一樣的是,一旦該讀寫鎖被寫解鎖,那麼全部因欲進行讀鎖定而被阻塞的Goroutine的運行都會被恢復。另外一方面,若是在進行過程當中發現當前的讀寫鎖已被讀鎖定,那麼這個寫鎖定操做將會等待直至全部施加於該讀寫鎖之上的讀鎖定都被清除。一樣的,在有多個寫鎖定操做爲此而等待的狀況下,相應的讀鎖定的所有清除只能讓其中的某一個寫鎖定操做得到進行的機會。

      如今來關注寫解鎖和讀解鎖。若是對一個未被寫鎖定的讀寫鎖進行寫解鎖,那麼會引起一個運行時恐慌。相似的,當對一個未被讀鎖定的讀寫鎖進行讀解鎖的時候也會引起一個運行時恐慌。寫解鎖在進行的同時會試圖喚醒全部因進行讀鎖定而被阻塞的Goroutine。而讀解鎖在進行的時候則會試圖喚醒一個因進行寫鎖定而被阻塞的Goroutine。

      不管鎖定針對的是寫操做仍是讀操做,咱們都應該儘可能及時的對相應的鎖進行解鎖。對於寫解鎖,咱們自沒必要多說。而讀解鎖的及時進行每每更容易被咱們忽視。雖然說讀解鎖的進行並不會對其餘正在進行中的讀操做產生任何影響,但它卻與相應的寫鎖定的進行關係緊密。注意,對於同一個讀寫鎖來講,施加在它之上的讀鎖定能夠有多個。所以,只有咱們對互斥鎖進行相同數量的讀解鎖,纔可以讓某一個相應的寫鎖定得到進行的機會。不然,後者會繼續使進行它的Goroutine處於阻塞狀態。因爲sync.RWMutex和*sync.RWMutex類型都沒有相應的方法讓咱們得到已進行的讀鎖定的數量,因此這裏是很容易出現問題的。還好咱們可使用defer語句來儘可能避免此類問題的發生。請記住,針對同一個讀寫鎖的寫鎖定和讀鎖定是互斥的。不管是寫解鎖仍是讀解鎖,操做的不及時都會對使用該讀寫鎖的流程的正常執行產生負面影響。

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

3、鎖的完整示例

     咱們下面來看一個與上述鎖實現有關的示例。在Go語言的標準庫代碼包os中有一個名爲File的結構體類型。os.File類型的值能夠被用來表明文件系統中的某一個文件或目錄。它的方法集合中包含了不少方法,其中的一些方法被用來對相應的文件進行寫操做和讀操做。

     假設,咱們須要建立一個文件來存放數據。在同一個時刻,可能會有多個Goroutine分別進行對此文件的進行寫操做和讀操做。每一次寫操做都應該向這個文件寫入若干個字節的數據。這若干字節的數據應該做爲一個獨立的數據塊存在。這就意味着,寫操做之間不能彼此干擾,寫入的內容之間也不能出現穿插和混淆的狀況。另外一方面,每一次讀操做都應該從這個文件中讀取一個獨立、完整的數據塊。它們讀取的數據塊不能重複,且須要按順序讀取。例如,第一個讀操做讀取了數據塊1,那麼第二個讀操做就應該去讀取數據塊2,而第三個讀操做則應該讀取數據塊3,以此類推。對於這些讀操做是否能夠被同時進行,這裏並不作要求。即便它們被同時進行,程序也應該分辨出它們的前後順序。

     爲了突出重點,咱們規定每一個數據塊的長度都是相同的。該長度應該在初始化的時候被給定。若寫操做實際欲寫入數據的長度超過了該值,則超出部分將會被截掉。

     當咱們拿到這樣一個需求的時候,首先應該想到使用os.File類型。它爲咱們操做文件系統中的文件提供了底層的支持。可是,該類型的相關方法並無對併發操做的安全性進行保證。換句話說,這些方法不是併發安全的。我只能經過額外的同步手段來保證這一點。鑑於這裏須要分別對兩類操做(即寫操做和讀操做)進行訪問控制,因此讀寫鎖在這裏會比普通的互斥鎖更加適用。不過,關於多個讀操做要按順序且不能重複讀取的這個問題,咱們需還要使用其餘輔助手段來解決。

      爲了實現上述需求,咱們須要建立一個類型。做爲該類型的行爲定義,咱們先編寫了一個這樣的接口:

複製代碼代碼以下:

// 數據文件的接口類型。

 

type DataFile interface {

// 讀取一個數據塊。

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

// 寫入一個數據塊。

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

// 獲取最後讀取的數據塊的序列號。

Rsn() int64

// 獲取最後寫入的數據塊的序列號。

Wsn() int64

// 獲取數據塊的長度

DataLen() uint32

}

 

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

 

複製代碼代碼以下:

// 數據的類型

 

type Data []byte

 

而名稱wsn和rsn分別是Writing Serial Number和Reading Serial Number的縮寫形式。它們分別表明了最後被寫入的數據塊的序列號和最後被讀取的數據塊的序列號。這裏所說的序列號至關於一個計數值,它會從1開始。所以,咱們能夠經過調用Rsn方法和Wsn方法獲得當前已被讀取和寫入的數據塊的數量。

根據上面對需求的簡單分析和這個DataFile接口類型聲明,咱們就能夠來編寫真正的實現了。咱們將這個實現類型命名爲myDataFile。它的基本結構以下:

複製代碼代碼以下:

// 數據文件的實現類型。

 

type myDataFile struct {

f       *os.File     // 文件。

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

woffset int64       // 寫操做須要用到的偏移量。

roffset int64       // 讀操做須要用到的偏移量。

wmutex sync.Mutex   // 寫操做須要用到的互斥鎖。

rmutex sync.Mutex   // 讀操做須要用到的互斥鎖。

dataLen uint32       // 數據塊長度。

}

 

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

關於這類函數的編寫,讀者應該已經得心應手了。NewDataFile函數會返回一個DataFile類型值,可是實際上它會建立並初始化一個*myDataFile類型的值並把它做爲它的結果值。這樣能夠經過編譯的緣由是,後者會是前者的一個實現類型。NewDataFile函數的完整聲明以下:

複製代碼代碼以下:

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

}

 

能夠看到,咱們在建立*myDataFile類型值的時候只須要對其中的字段f和dataLen進行初始化。這是由於woffset字段和roffset字段的零值都是0,而在未進行過寫操做和讀操做的時候它們的值理應如此。對於字段fmutex、wmutex和rmutex來講,它們的零值即爲可用的鎖。因此咱們也沒必要對它們進行顯式的初始化。

把變量df的值做爲NewDataFile函數的第一個結果值體現了咱們的設計意圖。但要想使*myDataFile類型真正成爲DataFile類型的一個實現類型,咱們還須要爲*myDataFile類型編寫出已在DataFile接口類型中聲明的全部方法。其中最重要的當屬Read方法和Write方法。

咱們先來編寫*myDataFile類型的Read方法。該方法應該按照以下步驟實現。

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

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

(3) 把該數據塊封裝成一個Data類型值並將其做爲結果值返回。

其中,前一個步驟在被執行的時候應該由互斥鎖rmutex保護起來。由於,咱們要求多個讀操做不能讀取同一個數據塊,而且它們應該按順序的讀取文件中的數據塊。而第二個步驟,咱們也會用讀寫鎖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()

 

//讀取一個數據塊

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)

 

的執行是互斥的。這是咱們爲了獲取到不重複且正確的讀偏移量所必需採起的措施。

另外一方面,在讀取一個數據塊的時候,咱們適時的進行了fmutex字段的讀鎖定和讀解鎖操做。fmutex字段的這兩個操做能夠保證咱們在這裏讀取到的是完整的數據塊。不過,這個完整的數據塊卻並不必定是正確的。爲何會這樣說呢?

請想象這樣一個場景。在咱們的程序中,有3個Goroutine來併發的執行某個*myDataFile類型值的Read方法,並有2個Goroutine來併發的執行該值的Write方法。經過前3個Goroutine的運行,數據文件中的數據塊被依次的讀取了出來。可是,因爲進行寫操做的Goroutine比進行讀操做的Goroutine少,因此過不了多久讀偏移量roffset的值就會等於甚至大於寫偏移量woffset的值。也就是說,讀操做很快就會沒有數據可讀了。這種狀況會使上面的df.f.ReadAt方法返回的第二個結果值爲表明錯誤的非nil且會與io.EOF相等的值。實際上,咱們不該該把這樣的值當作錯誤的表明,而應該把它當作一種邊界狀況。但不幸的是,咱們在這個版本的Read方法中並無對這種邊界狀況作出正確的處理。該方法在遇到這種狀況時會直接把錯誤值返回給它的調用方。該調用方會獲得讀取出錯的數據塊的序列號,但卻沒法再次嘗試讀取這個數據塊。因爲其餘正在或後續執行的Read方法會繼續增長讀偏移量roffset的值,因此當該調用方再次調用這個Read方法的時候只可能讀取到在此數據塊後面的其餘數據塊。注意,執行Read方法時遇到上述狀況的次數越多,被漏讀的數據塊也就會越多。爲了解決這個問題,咱們編寫了Read方法的第二個版本:

複製代碼代碼以下:

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

 

// 讀取並更新讀偏移量

// 省略若干條語句

//讀取一個數據塊

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方法展現中,咱們省略了若干條語句。緣由在這個位置上的那些語句並無任何變化。爲了進一步節省篇幅,咱們在後面也會遵循這樣的省略原則。

第二個版本的Read方法使用for語句是爲了達到這樣一個目的:在其中的df.f.ReadAt方法返回io.EOF錯誤的時候繼續嘗試獲取同一個數據塊,直到獲取成功爲止。注意,若是在該for代碼塊被執行期間一直讓讀寫鎖fmutex處於讀鎖定狀態,那麼針對它的寫鎖定操做將永遠不會成功,且相應的Goroutine也會被一直阻塞。由於它們是互斥的。因此,咱們不得不在該for語句塊中的每條return語句和continue語句的前面都加入一個針對該讀寫鎖的讀解鎖操做,並在每次迭代開始時都對fmutex進行一次讀鎖定。顯然,這樣的代碼看起來很醜陋。冗餘的代碼會使代碼的維護成本和出錯概率大大增長。而且,當for代碼塊中的代碼引起了運行時恐慌的時候,咱們是很難及時的對讀寫鎖fmutex進行讀解鎖的。即使能夠這樣作,那也會使Read方法的實現更加醜陋。咱們由於要處理一種邊界狀況而去掉了defer df.fmutex.RUnlock()語句。這種作法利弊參半。

其實,咱們能夠作得更好。可是這涉及到了其餘同步工具。所以,咱們之後再來對Read方法進行進一步的改造。順便提一句,當df.f.ReadAt方法返回一個非nil且不等於io.EOF的錯誤值的時候,咱們老是應該放棄再次獲取目標數據塊的嘗試而當即將該錯誤值返回給Read方法的調用方。由於這樣的錯誤極可能是嚴重的(好比,f字段表明的文件被刪除了),須要交由上層程序去處理。

如今,咱們來考慮*myDataFile類型的Write方法。與Read方法相比,Write方法的實現會簡單一些。由於後者不會涉及到邊界狀況。在該方法中,咱們須要進行兩個步驟,即:獲取並更新寫偏移量和向文件寫入一個數據塊。咱們直接給出Write方法的實現:

複製代碼代碼以下:

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

 

//寫入一個數據塊

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

defer df.fmutex.Unlock()

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

return

}

 

這裏須要注意的是,當參數d的值的長度大於數據塊的最大長度的時候,咱們會先進行截短處理再將數據寫入文件。若是沒有這個截短處理,咱們在後面計算的已讀數據塊的序列號和已寫數據塊的序列號就會不正確。

有了編寫前面兩個方法的經驗,咱們能夠很容易的編寫出*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)

}

 

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

至於*myDataFile類型的DataLen方法的實現,咱們無需呈現。它只是簡單地將dataLen字段的值做爲其結果值返回而已。

編寫上面這個完整示例的主要目的是展現互斥鎖和讀寫鎖在實際場景中的應用。因爲尚未講到Go語言提供的其餘同步工具,因此咱們在相關方法中全部須要同步的地方都是用鎖來實現的。然而,其中的一些問題用鎖來解決是不足夠或不合適的。咱們會在本節的後續部分中逐步的對它們進行改進。

從這兩種鎖的源碼中能夠看出,它們是同源的。讀寫鎖的內部是用互斥鎖來實現寫鎖定操做之間的互斥的。咱們能夠把讀寫鎖看作是互斥鎖的一種擴展。除此以外,這兩種鎖實如今內部都用到了操做系統提供的同步工具——信號燈。互斥鎖內部使用一個二值信號燈(只有兩個可能的值的信號燈)來實現鎖定操做之間的互斥,而讀寫鎖內部則使用一個二值信號燈和一個多值信號燈(能夠有多個可能的值的信號燈)來實現寫鎖定操做與讀鎖定操做之間的互斥。固然,爲了進行精確的協調,它們還使用到了其餘一些字段和變量。因爲篇幅緣由,咱們就不在這裏贅述了。若是讀者對此感興趣的話,能夠去閱讀sync代碼包中的相關源碼文件。

相關文章
相關標籤/搜索