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

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

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

     我想閱讀源代碼,不能僅僅只看到實現了什麼,還要看到做者的設計思路,還有若是你做爲做者,如何實現。這些纔是真正有用的東西,知識永遠學不完,咱們要鍛鍊咱們的思惟。算法

    要寫這篇文章的背景就忽略吧,我已經好久沒有寫博客了,主要緣由是我基本上看不到能讓我有所幫助的博客,更多的是我認爲我也寫不出能對別人有所幫助的文章。爲了寫這篇文章,我仍是花了挺多的心思收集歷史資料, 論壇討論,並去golang-nuts  上諮詢了一些問題。但願對你們有所幫助。windows

一. 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.htmapp

 

二. golang 最新版本的 sync.Mutexide

你能夠大體掃描一下最新版本的實現,若是你第一眼就看的很懂了,每步的操做?爲何這樣操做?有沒有更加合理的操做?那恭喜你,你的水平已經超過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點了,我必須睡覺了,今天也只能先回答半個問題了。至於爲何不是一個問題,而是半個問題,請聽下回分解。

相關文章
相關標籤/搜索