用喜歡和舒服的方式在Golang中使用鎖、使用channel自定義鎖

pathbox發表於2017-08-12

眾所周知,我們能使用Golang輕鬆編寫併發程式。Golang利用goroutine,讓我們編寫併發程式變得容易。併發程式中重要的問題之一就是如何正確的處理“競爭資源”或“共享資源”。Golang為我們提供了鎖的機制。這篇文章,就簡單介紹Golang中鎖的使用方法。並且進行錯誤的使用方法和正確的使用方法的程式碼示例對比。文章的所以程式碼示例在:https://github.com/pathbox/learning-go/tree/master/src/lock

原文連結

我們看第一個栗子:

package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    Value int
}

var wg sync.WaitGroup
var mutex sync.Mutex // 宣告瞭一個全域性鎖
func main() {

    wg.Add(1000)
    counter := &Counter{Value: 0}

    for i := 0; i < 1000; i++ {
        go Count(counter, mutex)
    }
    wg.Wait()
    fmt.Println("Count Value: ", counter.Value)
}

func Count(counter *Counter, mutex sync.Mutex) {
    mutex.Lock()
    defer mutex.Unlock()
    counter.Value++
    wg.Done()
}

/*
輸出結果:
Count Value:  982
*/

這裡宣告瞭一個全域性鎖 sync.Mutex,然後將這個全域性鎖以引數的方式代入到方法中,這樣並沒有真正起到加鎖的作用。

正確的方式是:

package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    Value int
}

var wg sync.WaitGroup
var mutex sync.Mutex // 宣告瞭一個全域性鎖
func main() {

    wg.Add(1000)
    counter := &Counter{Value: 0}

    for i := 0; i < 1000; i++ {
        go Count(counter)
    }
    wg.Wait()
    fmt.Println("Count Value: ", counter.Value)
}

func Count(counter *Counter) {
    mutex.Lock()
    defer mutex.Unlock()
    counter.Value++
    wg.Done()
}

/*
輸出結果:
Count Value:  1000
*/

宣告瞭一個全域性鎖後,其作用範圍是全域性。直接使用,而不是將其作為引數傳遞到方法中。

下一個栗子

package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    Value int
}

var wg sync.WaitGroup

func main() {
    var mutex sync.Mutex // 宣告瞭一個非全域性鎖
    wg.Add(1000)
    counter := &Counter{Value: 0}

    for i := 0; i < 1000; i++ {
        go Count(counter, mutex)
    }
    wg.Wait()
    fmt.Println("Count Value: ", counter.Value)
}

func Count(counter *Counter, mutex sync.Mutex) {
    mutex.Lock()
    defer mutex.Unlock()
    counter.Value++
    wg.Done()
}

/*
輸出結果:
Count Value:  954
*/

上面栗子中,宣告的不是全域性鎖。然後將這個鎖作為引數傳入到Count()方法中,這樣並沒有真正起到加鎖的作用。

正確的方式:

package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    Value int
}

var wg sync.WaitGroup

func main() {
    mutex := &sync.Mutex{} // 定義了一個鎖 mutex,賦值給mutex
    wg.Add(1000)
    counter := &Counter{Value: 0}

    for i := 0; i < 1000; i++ {
        go Count(counter, mutex)
    }
    wg.Wait()
    fmt.Println("Count Value: ", counter.Value)
}

func Count(counter *Counter, mutex *sync.Mutex) {
    mutex.Lock()
    defer mutex.Unlock()
    counter.Value++
    wg.Done()
}

/*
輸出結果:
Count Value:  1000
*/

這次通過 mutex := &sync.Mutex{},定義了mutex,然後作為引數傳遞到方法中,正確實現了加鎖功能。

簡單的說,在全域性宣告全域性鎖,之後這個全域性鎖就能在程式碼中的作用域範圍內都能使用了。但是,也許你需要的不是全域性鎖。這和鎖的粒度有關。 所以,你可以宣告一個鎖,在其作用域範圍內使用,並且這個作用域範圍是有併發執行的,別將鎖當成引數傳遞。如果,需要將鎖當成引數傳遞,那麼你傳的不是一個鎖的宣告,而是這個鎖的指標。

下面,我們討論一種更好的使用方式。通過閱讀過很多”牛人“寫的Go的程式或原始碼庫,在鎖的使用中。常常將鎖放入對應的 struct 中定義,我覺得這是一種不錯的方法。

package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    Value int
    sync.Mutex
}

var wg sync.WaitGroup

func main() {

    wg.Add(1000)
    counter := &Counter{Value: 0}

    for i := 0; i < 1000; i++ {
        go Count(counter)
    }
    wg.Wait()
    fmt.Println("Count Value: ", counter.Value)
}

func Count(counter *Counter) {
    counter.Lock()
    defer counter.Unlock()
    counter.Value++
    wg.Done()
}

/*
輸出結果:
Count Value:  1000
*/

這樣,我們宣告的不是全域性鎖,並且這個需要加鎖的競爭資源也正是 struct Counter 本身的Value屬性,反映了這個鎖的粒度。我覺得這是一種很舒服的使用方式(暫不知道這種方式會帶來什麼負面影響,如果有踩過坑的朋友,歡迎聊一聊這個坑),當然,如果你需要全域性鎖,那麼請定義全域性鎖。

還可以有更多的使用方式:

// 1.
type Counter struct {
   Value int
   Mutex sync.Mutex
}

counter := &Counter{Value: 0}
counter.Mutex.Lock()
defer counter.Mutex.Unlock()

//2.
type Counter struct {
   Value int
   Mutex *sync.Mutex
}

counter := &Counter{Value: 0, Mutex: &sync.Mutex{}}
counter.Mutex.Lock()
defer counter.Mutex.Unlock()

Choose the way you like~

接下來,我們自己嘗試建立一個互斥鎖。

簡單的說,簡單的互斥鎖鎖的原理是:一個執行緒(程式)拿到了這個互斥鎖,在這個時刻,只有這個執行緒(程式)能夠進行互斥鎖鎖的範圍中的"共享資源"的操作,主要是寫操作。我們這裡不討論讀鎖的實現。鎖的種類很多,有不同的實現場景和功能。這裡我們討論的是最簡單的互斥鎖。

我們能夠利用Golang 的channel所具有特性,建立一個簡單的互斥鎖。

/locker/locker.go

package locker

// Mutext struct
type Mutex struct {
    lock chan struct{}
}

// 建立一個互斥鎖
func NewMutex() *Mutex {
    return &Mutex{lock: make(chan struct{}, 1)}
}

// 鎖操作
func (m *Mutex) Lock() {
    m.lock <- struct{}{}
}

// 解鎖操作
func (m *Mutex) Unlock() {
    <-m.lock
}

main.go

package main

import (
    "./locker"
    "fmt"
    "time"
)

type record struct {
    lock          *locker.Mutex
    lock_count    int
    no_lock_count int
}

func newRecord() *record {
    return &record{
        lock:          locker.NewMutex(),
        lock_count:    0,
        no_lock_count: 0,
    }
}

func main() {
    r := newRecord()

    for i := 0; i < 1000; i++ {
        go CountWithoutLock(r)
        go CountWithLock(r)
    }
    time.Sleep(2 * time.Second)
    fmt.Println("Record no_lock_count: ", r.no_lock_count)
    fmt.Println("Record lock_count: ", r.lock_count)
}

func CountWithLock(r *record) {
    r.lock.Lock()
    defer r.lock.Unlock()
    r.lock_count++
}

func CountWithoutLock(r *record) {
    r.no_lock_count++
}

/* 輸出結果
Record no_lock_count:  995
Record lock_count:  1000
*/

locker 就是通過使用channel的讀操作和寫操作會互相阻塞等待的這個同步性質。 可以簡單的理解為,channel中傳遞的就是互斥鎖。一個執行緒(程式)申請了一個互斥鎖(struct{}{}),將這個互斥鎖存放在channel中, 其他執行緒(程式)就沒法申請互斥鎖放入channel,而處於阻塞狀態,等待channel恢復空閒空間。該執行緒(程式)進行操作”共享資源“,然後釋放這個互斥鎖(從channel中取走),channel這時候恢復了空閒的空間,其他執行緒(程式) 就能申請互斥鎖並且放入channel。這樣,在某一時刻,只會有一個執行緒(程式)擁有互斥鎖,在操作"共享資源"。

相關文章