亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

golang基于Mutex實現(xiàn)可重入鎖

 更新時間:2024年03月27日 09:11:13   作者:HC3244535  
鎖可重入也就是當前已經獲取到鎖的goroutine繼續(xù)調用Lock方法獲取鎖,Go標準庫中提供了sync.Mutex實現(xiàn)了排他鎖,但并不是可重入的,所以本文給大家介紹了golang基于Mutex實現(xiàn)可重入鎖,文中有詳細的代碼示例,需要的朋友可以參考下

golang基于Mutex實現(xiàn)可重入鎖

為什么需要可重入鎖

我們平時說的分布式鎖,一般指的是在不同服務器上的多個線程中,只有一個線程能搶到一個鎖,從而執(zhí)行一個任務。而我們使用鎖就是保證一個任務只能由一個線程來完成。所以我們一般是使用這樣的三段式邏輯:

Lock();
DoJob();
Unlock();

但是由于我們的系統(tǒng)都是分布式的,這個鎖一般不會只放在某個進程中,我們會借用第三方存儲,比如 Redis 來做這種分布式鎖。但是一旦借助了第三方存儲,我們就必須面對這個問題:Unlock是否能保證一定運行呢?

這個問題,我們面對的除了程序的bug之外,還有網絡的不穩(wěn)定,進程被殺死,服務器被down機等。我們是無法保證Unlock一定被運行的。

那么我們就一般在Lock的時候為這個鎖加一個超時時間作為兜底。

LockByExpire(duration);
DoJob();
Unlock();

這個超時時間是為了一旦出現(xiàn)異常情況導致Unlock沒有被運行,這個鎖在duration時間內也會被自動釋放。這個在redis中我們一般就是使用set ex 來進行鎖超時的設定。

但是有這個超時時間我們又遇上了問題,超時時間設置多久合適呢?當然要設置的比 DoJob 消耗的時間更長,否則的話,在任務還沒結束的時候,鎖就被釋放了,還是有可能導致并發(fā)任務的存在。

但是實際上,同樣由于網絡超時問題,系統(tǒng)運行狀況問題等,我們是無法準確知道DoJob這個函數(shù)要執(zhí)行多久的。那么這時候怎么辦呢?

有兩個辦法:

第一個方法,我們可以對DoJob做一個超時設置。讓DoJob最多只能執(zhí)行n秒,那么我的分布式鎖的超時時長設置比n秒長就可以了。為一個任務設置超時時間在很多語言是可以做到的。比如golang 中的 TimeoutContext。

而第二種方法,就是我們先為鎖設置一個比較小的超時時長,然后不斷續(xù)期這個鎖。對一個鎖的不斷需求,也可以理解為重新開始加鎖,這種可以不斷續(xù)期的鎖,就叫做可重入鎖。

除了主線程之外,可重入鎖必然有一個另外的線程(或者攜程)可以對這個鎖進行續(xù)期,我們叫這個額外的程序叫做watchDog(看門狗)。

鎖重入的定義

鎖可重入也就是當前已經獲取到鎖的goroutine繼續(xù)調用Lock方法獲取鎖,Go標準庫中提供了sync.Mutex實現(xiàn)了排他鎖,但并不是可重入的,如果在代碼中重入鎖,也就是Lock之后再次進行Lock獲取鎖,則會被阻塞到第二次Lock上,鎖沒有辦法得到釋放從而影響其它goroutine執(zhí)行

// 例如
package main;

import "sync"

func ReentryExample() {
	var c int64
	var mu sync.Mutex
	mu.Lock() // 第一次加鎖
	// TODO //
	mu.Lock() // 第二次加鎖,阻塞
	c++;
	// TODO ...
}

重入鎖的簡單實現(xiàn)思路

  • 拿到能夠識別到當前協(xié)程的id,(通過堆棧信息獲取到goroutine的id)
  • 寫一個結構體,實現(xiàn)Locker接口

首先獲取到goroutine的id

func GoID() int {
	var buf [32]byte
	n := runtime.Stack(buf[:],false) // 獲取堆棧的信息
	// string(buf[:n] 
	/**
	 goroutine 6 [running]:
	 main.XXX	
	*/
	// 拿到goroutine的id
	goIdStr := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine"))[0]
	goId, err := strconv.Atoi(fieldId)// 轉換為int
	return goId
}

然后開始編寫可重入鎖的結構體

// ReentrantMutex 可重入的互斥鎖
type ReentrantMutex struct {
	sync.Mutex       // 互斥鎖
	goId       int64 // 用于保存goroutine的id
	recursion  int64 // 鎖重入的次數(shù)
}

// Lock 實現(xiàn)Locker接口,用于加鎖
func (r *ReentrantMutex) Lock() {
	gid := GoID()
	if atomic.LoadInt64(&r.goId) == gid { // 看看是否已經加過鎖了?
		atomic.AddInt64(&r.recursion, 1) // 如果之前加過鎖,則重入的次數(shù)+1
		return
	}
	r.Mutex.Lock() // 使用互斥鎖上鎖
	atomic.StoreInt64(&r.goId, gid) // 使用原子操作保存goroutine的id
	atomic.StoreInt64(&r.recursion, 1) // 第一次加鎖,因此重入的次數(shù)為一
}

// Unlock 實現(xiàn)了Locker的接口,用于解鎖
func (r *ReentrantMutex) Unlock() {
	gid := GoID()
	if atomic.LoadInt64(&r.goId) != gid { // 看是否加過鎖
		panic("未加鎖") // 沒有加過鎖,不存在解鎖,直接panic
	}
	recursion := atomic.AddInt64(&r.recursion, -1) // 重入次數(shù)-1
	if recursion != 0 { // 如果重入次數(shù)沒有等于0(意味著還有鎖沒有釋放)
		return
	}
	atomic.StoreInt64(&r.goId, -1) // 重入次數(shù)為0,則不存在鎖沒有釋放,解鎖
	r.Mutex.Unlock() // 互斥鎖解鎖
}

測試用例

package main;

func main() {
	var m ReentrantMutex
	m.Lock()
	m.Lock() // 不會阻塞
	fmt.Println("1") // 正常打印1
	m.Unlock()
	m.Unlock()// 解鎖
}

其他方法實現(xiàn)Golang可重入鎖:

具體實現(xiàn)

在Golang中,語言級別天生支持協(xié)程,所以這種可重入鎖就非常容易實現(xiàn):

// DistributeLockRedis 基于redis的分布式可重入鎖,自動續(xù)租
type DistributeLockRedis struct {
 key       string             // 鎖的key
 expire    int64              // 鎖超時時間
 status    bool               // 上鎖成功標識
 cancelFun context.CancelFunc // 用于取消自動續(xù)租攜程
 redis     redis.Client       // redis句柄
}
 
// 創(chuàng)建可
func NewDistributeLockRedis(key string, expire int64) *DistributeLockRedis {
 return &DistributeLockRedis{
   key : key,
   expire : expire,
 }
}
 
// TryLock 上鎖
func (dl *DistributeLockRedis) TryLock() (err error) {
 if err = dl.lock(); err != nil {
  return err
 }
 ctx, cancelFun := context.WithCancel(context.Background())
 dl.cancelFun = cancelFun
 dl.startWatchDog(ctx) // 創(chuàng)建守護協(xié)程,自動對鎖進行續(xù)期
 dl.status = true
 return nil
}
 
// competition 競爭鎖
func (dl *DistributeLockRedis) lock() error {
 if res, err := redis.String(dl.redis.Do(context.Background(), "SET", dl.key, 1, "NX", "EX", dl.expire)); err != nil {
  return err
 } 
 return nil
}
 
 
// guard 創(chuàng)建守護協(xié)程,自動續(xù)期
func (dl *DistributeLockRedis) startWatchDog(ctx context.Context) {
 safeGo(func() error {
  for {
   select {
   // Unlock通知結束
   case <-ctx.Done():
    return nil
   default:
    // 否則只要開始了,就自動重入(續(xù)租鎖)
    if dl.status {
     if res, err := redis.Int(dl.redis.Do(context.Background(), "EXPIRE", dl.key, dl.expire)); err != nil {
      return nil
     } 
     // 續(xù)租時間為 expire/2 秒
     time.Sleep(time.Duration(dl.expire/2) * time.Second)
    }
   }
  }
 })
}
 
// Unlock 釋放鎖
func (dl *DistributeLockRedis) Unlock() (err error) {
 // 這個重入鎖必須取消,放在第一個地方執(zhí)行
 if dl.cancelFun != nil {
  dl.cancelFun() // 釋放成功,取消重入鎖
 }
 var res int
 if dl.status {
  if res, err = redis.Int(dl.redis.Do(context.Background(), "Del", dl.key)); err != nil {
   return fmt.Errorf("釋放鎖失敗")
  }
  if res == 1 {
   dl.status = false
   return nil
  }
 }
 return fmt.Errorf("釋放鎖失敗")
}

這段代碼的邏輯基本上都以注釋的形式來寫了。其中主要就在startWatchDog,對鎖進行重新續(xù)期

ctx, cancelFun := context.WithCancel(context.Background())
dl.cancelFun = cancelFun
dl.startWatchDog(ctx) // 創(chuàng)建守護協(xié)程,自動對鎖進行續(xù)期
dl.status = true

首先創(chuàng)建一個cancelContext,它的context函數(shù)cancelFunc是給Unlock進行調用的。然后啟動一個goroutine進程來循環(huán)續(xù)期。

這個新啟動的goroutine在主goroutine處理結束,調用Unlock的時候,才會結束,否則會在 過期時間/2 的時候,調用一次redis的expire命令來進行續(xù)期。

至于外部,在使用的時候如下

func Foo() error {
  key := foo
  
  // 創(chuàng)建可重入的分布式鎖
 dl := NewDistributeLockRedis(key, 10)
 // 爭搶鎖
 err := dl.TryLock()
 if err != nil {
  // 沒有搶到鎖
  return err
 }
 
 // 搶到鎖的記得釋放鎖
 defer func() {
  dl.Unlock()
 }
 
 // 做真正的任務
 DoJob()
}

到此這篇關于golang基于Mutex實現(xiàn)可重入鎖的文章就介紹到這了,更多相關golang Mutex可重入鎖內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • Go語言基礎學習之指針詳解

    Go語言基礎學習之指針詳解

    Go 語言中指針是很容易學習的,Go 語言中使用指針可以更簡單的執(zhí)行一些任務。所以本文就來和大家聊聊Go語言中指針的定義與使用,需要的可以參考一下
    2022-12-12
  • golang獲取客戶端ip的實現(xiàn)

    golang獲取客戶端ip的實現(xiàn)

    本文主要介紹了golang獲取客戶端ip的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2023-07-07
  • Golang實現(xiàn)自己的Redis(pipeline客戶端)實例探索

    Golang實現(xiàn)自己的Redis(pipeline客戶端)實例探索

    這篇文章主要為大家介紹了Golang實現(xiàn)自己的Redis(pipeline客戶端)實例探索,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2024-01-01
  • Golang拾遺之指針和接口的使用詳解

    Golang拾遺之指針和接口的使用詳解

    拾遺主要是收集和golang相關的瑣碎知識,這篇文章主要是為大家整理了Golang中指針和接口的使用方法,文中的示例代碼講解詳細,需要的可以參考一下
    2023-02-02
  • Go常用技能日志log包創(chuàng)建使用示例

    Go常用技能日志log包創(chuàng)建使用示例

    這篇文章主要為大家介紹了Go常用技能日志log包創(chuàng)建使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-08-08
  • 詳解Go語言的錯誤處理和資源管理

    詳解Go語言的錯誤處理和資源管理

    資源處理是什么?打開文件需要關閉,打開數(shù)據(jù)庫連接,連接需要釋放。這些成對出現(xiàn)的就是資源管理。有時候我們雖然釋放了,但是程序在中間出錯了,那么可能導致資源釋放失敗。如何保證打開的文件一定會被關閉呢?這就是資源管理與錯誤處理考慮的一個原因
    2021-06-06
  • Go打包附件內容到執(zhí)行文件的方法

    Go打包附件內容到執(zhí)行文件的方法

    處于種種原因, 我們不希望這部分額外的內容以附件的形式出現(xiàn), 有沒有什么辦法能夠將附件內容直接打包進可執(zhí)行文件中呢,下面小編給大家介紹下Go打包附件內容到執(zhí)行文件的方法,感興趣的朋友一起看看吧
    2023-03-03
  • golang默認Logger日志庫在項目中使用Zap日志庫

    golang默認Logger日志庫在項目中使用Zap日志庫

    這篇文章主要為大家介紹了golang默認Logger日志庫在項目中使用Zap日志庫,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步早日升職加薪
    2022-04-04
  • Golang中errgroup的常見誤用詳解

    Golang中errgroup的常見誤用詳解

    errgroup和sync.WaitGroup類似,都可以發(fā)起執(zhí)行并等待一組協(xié)程直到所有協(xié)程運行結束,本文主要為大家整理了一些errgroup的常見誤用,有需要的可以參考下
    2024-01-01
  • Golang的strings.Split()踩坑記錄

    Golang的strings.Split()踩坑記錄

    工作中,當我們需要對字符串按照某個字符串切分成字符串數(shù)組數(shù)時,常用到strings.Split(),本文主要介紹了Golang的strings.Split()踩坑記錄,感興趣的可以了解一下
    2022-05-05

最新評論