這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
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。我還經常用來分析複雜的多線程鎖定問題,獨家秘訣,今天在這裡泄露了。 第一個程式可以抽象出這樣一個圖: 這個狀態機器非常簡單,有兩種狀態(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, 不過,我靠,貌似只是加了一個狀態,圖複雜了這樣多,理論上,這是一個無限狀態自動機了,但是實際上,同時等待的數目一般不會是無限的。其實要證明為什麼這個程式是正確的,從圖上應該可以看出思路了。LSE都是 向上的,USE都是向下的。所以,Lse操作後,要想再有個Lse,必須先操作一個Use。所以,證明的關鍵還在於sema的特性,基本上可以把狀態忽略,當然, 從0,0 到 1,0 這是一個非常特殊的狀態,他們和訊號量無關。如果你是golang的忠實粉絲,而且從09年就開始知道golang的話,那麼你一定知道 第二個程式就是 golang類庫中最初始的 Mutex版本。比現在的版本要簡單很多,但是效能上要慢一點點。看類庫的演化其實是一件非常有趣的事情,我比較喜歡看非常原始的版本, 而不喜歡看最新版本的原始碼,因為最新版本,成熟的版本,往往包括了太多的效能最佳化的細節,而損失了可讀性, 也難以從中得到有用的思想。
理解一個程式如何工作很簡單,但是,作者的設計思路才是關鍵,我們可以不斷的看原始碼,看別人的實現,我們能從中學到很多知識與技巧,當遇到相同的問題的時候,我們也能解決類似的問題。
我個人覺得,作為一個天朝的程式員,不能僅僅是山寨別人的軟體,學習別人的東西。還是要能進入一個新的領域,一個未知的領域,還能有所創新。
當然,作者的設計思路我們很難得知,我們看到的只是勞動的結果,但是,我們可以這樣問自己,如果我是作者,我怎麼思考這個問題,然後解決這個問題。我發現,用這樣的思維去考慮問題,有時候能給我很多的啟示。
還有五分鐘就12點了,我必須睡覺了,今天也只能先回答半個問題了。至於為什麼不是一個問題,而是半個問題,請聽下回分解。