gf框架之gmlock – 記憶體鎖模組

John發表於2019-02-16

文章來源:http://gf.johng.cn/os/gmlock/…

記憶體鎖。該模組包含兩個物件特性:

  1. Locker 記憶體鎖,支援按照給定鍵名生成記憶體鎖,並支援Try*Lock鎖過期特性;
  2. Mutex 對標準庫底層sync.Mutex的封裝,增加了Try*Lock特性;

使用方式

import "gitee.com/johng/gf/g/os/gmlock"

使用場景

  1. 任何需要併發安全的場景,可以替代sync.Mutex
  2. 需要使用Try*Lock的場景(不需要阻塞等待鎖釋放);
  3. 需要動態建立互斥鎖,或者需要維護大量動態鎖的場景;

方法列表

func Lock(key string, expire ...int)
func RLock(key string, expire ...int)
func RUnlock(key string)
func TryLock(key string, expire ...int) bool
func TryRLock(key string, expire ...int) bool
func Unlock(key string)
type Locker
    func New() *Locker
    func (l *Locker) Lock(key string, expire ...int)
    func (l *Locker) RLock(key string, expire ...int)
    func (l *Locker) RUnlock(key string)
    func (l *Locker) TryLock(key string, expire ...int) bool
    func (l *Locker) TryRLock(key string, expire ...int) bool
    func (l *Locker) Unlock(key string)
type Mutex
    func NewMutex() *Mutex
    func (l *Mutex) Lock()
    func (l *Mutex) RLock()
    func (l *Mutex) RUnlock()
    func (l *Mutex) TryLock() bool
    func (l *Mutex) TryRLock() bool
    func (l *Mutex) Unlock()

示例1,基本使用

package main

import (
    "time"
    "sync"
    "gitee.com/johng/gf/g/os/glog"
    "gitee.com/johng/gf/g/os/gmlock"
)

func main() {
    key := "lock"
    wg  := sync.WaitGroup{}
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            gmlock.Lock(key)
            glog.Println(i)
            time.Sleep(time.Second)
            gmlock.Unlock(key)
            wg.Done()
        }(i)
    }
    wg.Wait()
}

該示例中,模擬了同時開啟10個goroutine,但同一時刻只能有一個goroutine獲得鎖,獲得鎖的goroutine執行1秒後退出,其他goroutine才能獲得鎖。

執行後,輸出結果為:

2018-10-15 23:57:28.295 9
2018-10-15 23:57:29.296 0
2018-10-15 23:57:30.296 1
2018-10-15 23:57:31.296 2
2018-10-15 23:57:32.296 3
2018-10-15 23:57:33.297 4
2018-10-15 23:57:34.297 5
2018-10-15 23:57:35.297 6
2018-10-15 23:57:36.298 7
2018-10-15 23:57:37.298 8

示例2,過期控制

我們將以上的示例使用過期時間控制來實現。

package main

import (
    "sync"
    "gitee.com/johng/gf/g/os/glog"
    "gitee.com/johng/gf/g/os/gmlock"
)

func main() {
    key := "lock"
    wg  := sync.WaitGroup{}
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            gmlock.Lock(key, 1000)
            glog.Println(i)
            wg.Done()
        }(i)
    }
    wg.Wait()
}

執行後,輸出結果為:

2018-10-15 23:59:14.663 9
2018-10-15 23:59:15.663 4
2018-10-15 23:59:16.663 0
2018-10-15 23:59:17.664 1
2018-10-15 23:59:18.664 2
2018-10-15 23:59:19.664 3
2018-10-15 23:59:20.664 6
2018-10-15 23:59:21.664 5
2018-10-15 23:59:22.665 7
2018-10-15 23:59:23.665 8

示例3,TryLock非阻塞鎖

TryLock方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true;如果獲取失敗(即鎖已被其他goroutine獲取),則返回false

package main

import (
    "sync"
    "gitee.com/johng/gf/g/os/glog"
    "time"
    "gitee.com/johng/gf/g/os/gmlock"
)

func main() {
    key := "lock"
    wg  := sync.WaitGroup{}
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            if gmlock.TryLock(key) {
                glog.Println(i)
                time.Sleep(time.Second)
                gmlock.Unlock(key)
            } else {
                glog.Println(false)
            }
            wg.Done()
        }(i)
    }
    wg.Wait()
}

同理,在該示例中,同時也只有1個goroutine能獲得鎖,其他goroutine在TryLock失敗便直接退出了。

執行後,輸出結果為:

2018-10-16 00:01:59.172 9
2018-10-16 00:01:59.172 false
2018-10-16 00:01:59.172 false
2018-10-16 00:01:59.172 false
2018-10-16 00:01:59.172 false
2018-10-16 00:01:59.172 false
2018-10-16 00:01:59.172 false
2018-10-16 00:01:59.172 false
2018-10-16 00:01:59.172 false
2018-10-16 00:01:59.176 false

示例4,多個鎖機制衝突

該示例用來演示在複雜邏輯下的鎖機制處理情況。

package main

import (
    "gitee.com/johng/gf/g/os/gmlock"
    "time"
    "gitee.com/johng/gf/g/os/glog"
    "fmt"
)

// 記憶體鎖 - 手動Unlock與計時Unlock衝突校驗
func main() {
    key := "key"

    // 第一次鎖帶時間
    gmlock.Lock(key, 1000)
    glog.Println("lock1")
    // 這個時候上一次的計時解鎖已失效
    gmlock.Unlock(key)
    glog.Println("unlock1")

    fmt.Println()

    // 第二次鎖,不帶時間,且在執行過程中前一個Lock的定時解鎖生效
    gmlock.Lock(key)
    glog.Println("lock2")
    go func() {
        // 正常情況下3秒後才能執行這句
        gmlock.Lock(key)
        glog.Println("lock by goroutine")
    }()
    time.Sleep(3*time.Second)
    // 這時再解鎖
    gmlock.Unlock(key)
    // 注意3秒之後才會執行這一句
    glog.Println("unlock2")

    // 阻塞程式
    select{}
}

執行後,輸出結果為:

2018-10-16 00:03:40.277 lock1
2018-10-16 00:03:40.279 unlock1

2018-10-16 00:03:40.279 lock2
2018-10-16 00:03:43.279 unlock2
2018-10-16 00:03:43.279 lock by goroutine

示例5,多檔案併發寫的安全控制

glog模組寫日誌檔案的時候有這麼一個核心方法,我們拿來看一下(原始碼位於 /g/os/glog/glog_logger.go)。

// 這裡的寫鎖保證同一時刻只會寫入一行日誌,防止串日誌的情況
func (l *Logger) print(std io.Writer, s string) {
    // 優先使用自定義的IO輸出
    if l.printHeader.Val() {
        s = l.format(s)
    }
    writer := l.GetWriter()
    if writer == nil {
        // 如果設定的writer為空,那麼其次判斷是否有檔案輸出設定
        // 內部使用了記憶體鎖,保證在glog中對同一個日誌檔案的併發寫入不會串日誌(併發安全)
        if f := l.getFilePointer(); f != nil {
            defer f.Close()
            key := l.path.Val()
            gmlock.Lock(key)
            _, err := io.WriteString(f, s)
            gmlock.Unlock(key)
            if err != nil {
                fmt.Fprintln(os.Stderr, err.Error())
            }
        }
    } else {
        l.doStdLockPrint(writer, s)
    }
    // 是否允許輸出到標準輸出
    if l.alsoStdPrint.Val() {
        l.doStdLockPrint(std, s)
    }
}

其中的:

gmlock.Lock(key)
...
gmlock.Unlock(key)

便使用到了記憶體鎖的特性,其中的變數key表示的是日誌檔案的絕對路徑,當多個goroutine對同一個日誌檔案進行寫入時,由gmlock.Lock(key)來保證對該檔案的併發安全寫操作。

相關文章