go sync.Mutex 設計思想與演化過程 (一)


     go語言在雲計算時代將會如日中天,還抱着.NET不放的人將會被淘汰。學習go語言和.NET完全不一樣,它有非常簡單的runtime 和 類庫。最好的辦法就是將整個源代碼讀一遍,這是我見過最簡潔的系統類庫。讀了之后,你會真正體會到C#的面向對象的表達方式是有問題的,繼承並不是必要的東西。相同的問題,在go中有更加簡單的表達。

  go runtime 沒有提供任何的鎖,只是提供了一個PV操作原語。獨占鎖,條件鎖 都是基於這個原語實現的。如果你學習了go,那就就知道如何在windows下高效的方式實現條件鎖定(windows沒有自帶的條件鎖)。

     我想閱讀源代碼,不能僅僅只看到實現了什么,還要看到作者的設計思路,還有如果你作為作者,如何實現。這些才是真正有用的東西,知識永遠學不完,我們要鍛煉我們的思維。

    要寫這篇文章的背景就忽略吧,我已經很久沒有寫博客了,主要原因是我基本上看不到能讓我有所幫助的博客,更多的是我認為我也寫不出能對別人有所幫助的文章。為了寫這篇文章,我還是花了挺多的心思收集歷史資料, 論壇討論,並去golang-nuts  上咨詢了一些問題。希望對大家有所幫助。

一. sync.Mutex 是什么?

Mutex是一種獨占鎖,一般操作系統都會提供這種鎖。但是,操作系統的鎖是針對線程的,golang里面沒有線程的概念,這樣操作系統的鎖就用不上了。所以,你看go語言的runtime,就會發現,實際上這是一個“操作系統”。如果Mutex還不知道的話,我建議看下面的文章,其中第一篇必看。

百度百科 mutex http://baike.baidu.com/view/1461738.htm?fromId=1889552&redirected=seachword

信號量:http://swtch.com/semaphore.pdf

還可以讀一下百度百科 pv 操作:http://baike.baidu.com/view/703687.htm

 

二. golang 最新版本的 sync.Mutex

你可以大致掃描一下最新版本的實現,如果你第一眼就看的很懂了,每步的操作?為什么這樣操作?有沒有更加合理的操作?那恭喜你,你的水平已經超過google實現 sync.Mutex 的程序員了,甚至是大部分的程序員,因為這個程序歷經幾年的演化,才到了今天的樣子,你第一眼就能看的如此透徹,那真的是很了不起。下面的章節是為沒有看懂的人准備的。

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package sync provides basic synchronization primitives such as mutual
// exclusion locks. Other than the Once and WaitGroup types, most are intended
// for use by low-level library routines. Higher-level synchronization is
// better done via channels and communication.
//
// Values containing the types defined in this package should not be copied.
package sync

import (
"sync/atomic"
"unsafe"
)

// A Mutex is a mutual exclusion lock.
// Mutexes can be created as part of other structures;
// the zero value for a Mutex is an unlocked mutex.
type Mutex struct {
state int32
sema uint32
}

// A Locker represents an object that can be locked and unlocked.
type Locker interface {
Lock()
Unlock()
}

const (
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexWaiterShift = iota
)

// Lock locks m.
// If the lock is already in use, the calling goroutine
// blocks until the mutex is available.
func (m *Mutex) Lock() {
// Fast path: grab unlocked mutex.
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if raceenabled {
raceAcquire(unsafe.Pointer(m))
}
return
}

awoke := false
for {
old := m.state
new := old | mutexLocked
if old&mutexLocked != 0 {
new = old + 1<<mutexWaiterShift
}
if awoke {
// The goroutine has been woken from sleep,
// so we need to reset the flag in either case.
new &^= mutexWoken
}
if atomic.CompareAndSwapInt32(&m.state, old, new) {
if old&mutexLocked == 0 {
break
}
runtime_Semacquire(&m.sema)
awoke = true
}
}

if raceenabled {
raceAcquire(unsafe.Pointer(m))
}
}

// Unlock unlocks m.
// It is a run-time error if m is not locked on entry to Unlock.
//
// A locked Mutex is not associated with a particular goroutine.
// It is allowed for one goroutine to lock a Mutex and then
// arrange for another goroutine to unlock it.
func (m *Mutex) Unlock() {
if raceenabled {
_ = m.state
raceRelease(unsafe.Pointer(m))
}

// Fast path: drop lock bit.
new := atomic.AddInt32(&m.state, -mutexLocked)
if (new+mutexLocked)&mutexLocked == 0 {
panic("sync: unlock of unlocked mutex")
}

old := new
for {
// If there are no waiters or a goroutine has already
// been woken or grabbed the lock, no need to wake anyone.
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken) != 0 {
return
}
// Grab the right to wake someone.
new = (old - 1<<mutexWaiterShift) | mutexWoken
if atomic.CompareAndSwapInt32(&m.state, old, new) {
runtime_Semrelease(&m.sema)
return
}
old = m.state
}
}

三. 有沒有更加簡潔的實現方法?

有點操作系統知識的都知道,獨占鎖是一種特殊的PV 操作,就 0 – 1 PV操作。那我想,如果不考慮任何性能問題的話,用信號量應該就可以這樣實現Mutex:

type Mutex struct {
sema uint32
}

func NewMutex() *Mutex {
var mu Mutex
mu.sema = 1
return &mu
}

func (m *Mutex) Lock() {
runtime_Semacquire(&m.sema)
}

func (m *Mutex2) Unlock() {
runtime_Semrelease(&m.sema)
}

當然,這個實現有點不符合要求。如果有個家伙不那么靠譜,加鎖了一次,但是解鎖了兩次。第二次解鎖的時候,應該報出一個錯誤,而不是讓錯誤隱藏。於是乎,我們想到用一個變量表示加鎖的次數。這樣就可以判斷有沒有多次解鎖。於是乎,我就想到了下面的解決方案:

type Mutex struct {
key int32
sema uint32
}

func (m *Mutex) Lock() {
if atomic.AddInt32(&m.key, 1) == 1 {
// changed from 0 to 1; we hold lock
return
}
runtime_Semacquire(&m.sema)
}

func (m *Mutex) Unlock() {
switch v := atomic.AddInt32(&m.key, -1); {
case v == 0:
// changed from 1 to 0; no contention
return
case v == -1:
// changed from 0 to -1: wasn't locked
// (or there are 4 billion goroutines waiting)
panic("sync: unlock of unlocked mutex")
}
runtime_Semrelease(&m.sema)
}
這個解決方案除了解決了我們前面說的重復加鎖的問題外,還對我們初始化工作做了簡化,不需要構造函數了。注意,這也是golang里面一個常見的設計模式,叫做 零初始化。
 
表示多線程復雜狀態,最好的辦法就是抽象出 狀態 和 操作,忽略掉線程,讓問題變成一個狀態機問題。這樣的圖不僅僅用於分析Mutex。我還經常用來分析復雜的多線程鎖定問題,獨家秘訣,今天在這里泄露了。
 
第一個程序可以抽象出這樣一個圖:
 
image
這個狀態機非常簡單,有兩種狀態(1, 0),兩個操作(Lock, Unlock)。A線程 Lock操作后,只要它不進行UnLock操作,就不可能有其他的線程能獲取到鎖。因為,這個狀態機唯一的軌跡是:Lock –-unlock --lock --unlock。
 
第二個程序可能的狀態會非常的多,不過要注意的是 程序 2 的 Lock 和 Unlock都不是原子操作,都會分成兩個部分。
Lock操作分成兩個部分,一個是更改鎖的狀態, 我們用LSt(Lock state change) 表示,一個是更改sema, LSe (Lock sema acquire)
unlock也是一樣,分別用USt (unlock state change), USe (unlock sema release) 表示。
 
那就是有4個操作,n種狀態在4種操作下不斷的切換, 如果  線程A 加鎖 -- 解鎖  中,其他線程不能進行 加鎖的完整操作(LSt + LSe)(可以進行部分的加鎖操作,比如LSt 操作), 那么程序就是正確的。
像這類最基礎的類庫,代碼量也不是很多的情況下,證明正確性是非常重要的。在我開發金融交易服務器的過程中,對很多關鍵的代碼我都進行了證明,我發現這是理解問題和發現bug的好方法。 這也是獨家的秘訣,在這里就泄露了。
說句題外話,有時間的話,一定要把 《算法導論》 里面的每一個證明都看的很通透,那你的水平就可以提升一大截了。上面對代碼的抽象是十分關鍵的技巧,這樣,就可以對這個代碼進行分析了。
 
程序2 圖表 : 注, 0,0 表示的是 key = 0, sema = 0,
image
 
不過,我靠,貌似只是加了一個狀態,圖復雜了這樣多,理論上,這是一個無限狀態自動機了,但是實際上,同時等待的數目一般不會是無限的。其實要證明為什么這個程序是正確的,從圖上應該可以看出思路了。LSE都是 向上的,USE都是向下的。所以,Lse操作后,要想再有個Lse,必須先操作一個Use。所以,證明的關鍵還在於sema的特性,基本上可以把狀態忽略,當然, 從0,0 到 1,0 這是一個非常特殊的狀態,他們和信號量無關。
如果你是golang的忠實粉絲,而且從09年就開始知道golang的話,那么你一定知道 第二個程序就是 golang類庫中最初始的 Mutex版本。比現在的版本要簡單很多,但是性能上要慢一點點。看類庫的演化其實是一件非常有趣的事情,我比較喜歡看非常原始的版本, 而不喜歡看最新版本的源代碼,因為最新版本,成熟的版本,往往包括了太多的性能優化的細節,而損失了可讀性, 也難以從中得到有用的思想。

    理解一個程序如何工作很簡單,但是,作者的設計思路才是關鍵,我們可以不斷的看源代碼,看別人的實現,我們能從中學到很多知識與技巧,當遇到相同的問題的時候,我們也能解決類似的問題。

我個人覺得,作為一個天朝的程序員,不能僅僅是山寨別人的軟件,學習別人的東西。還是要能進入一個新的領域,一個未知的領域,還能有所創新。

當然,作者的設計思路我們很難得知,我們看到的只是勞動的結果,但是,我們可以這樣問自己,如果我是作者,我怎么思考這個問題,然后解決這個問題。我發現,用這樣的思維去考慮問題,有時候能給我很多的啟示。

    還有五分鍾就12點了,我必須睡覺了,今天也只能先回答半個問題了。至於為什么不是一個問題,而是半個問題,請聽下回分解。


免責聲明!

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



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