Go语言 锁的介绍


锁的介绍与使用

1 互斥锁
传统并发程序对共享资源进行访问控制的主要手段,由标准库代码包中sync中的Mutex结构体表示。

//Mutex  是互斥锁, 零值是解锁的互斥锁, 首次使用后不得复制互斥锁。
type Mutex struct {
   state int32
   sema  uint32
}

sync.Mutex类型只有两个公开的指针方法

//Locker表示可以锁定和解锁的对象。
type Locker interface {
   Lock()
   Unlock()
}

//锁定当前的互斥量
//如果锁已被使用,则调用goroutine
//阻塞直到互斥锁可用。
func (m *Mutex) Lock() 

//对当前互斥量进行解锁
//如果在进入解锁时未锁定m,则为运行时错误。
//锁定的互斥锁与特定的goroutine无关。
//允许一个goroutine锁定Mutex然后安排另一个goroutine来解锁它。
func (m *Mutex) Unlock()

声明一个互斥锁:

var mutex sync.Mutex

不像C或Java的锁类工具,我们可能会犯一个错误:忘记及时解开已被锁住的锁,从而导致流程异常。但Go由于存在defer,所以此类问题出现的概率极低。关于defer解锁的方式如下:

var mutex sync.Mutex
func Write()  {
   mutex.Lock()
   defer mutex.Unlock()
}

如果对一个已经上锁的对象再次上锁,那么就会导致该锁定操作被阻塞,直到该互斥锁回到被解锁状态

func main()  {
   var mutex sync.Mutex
   fmt.Println("start lock main")
   mutex.Lock()
   fmt.Println("get locked main")
   for i := 1;i<=3 ;i++  {
      go func(i int) {  
         fmt.Println("start lock ",i)
         mutex.Lock()
         fmt.Println("get locked ",i)
      }(i)
   }

   time.Sleep(time.Second)
   fmt.Println("Unlock the lock main")
   mutex.Unlock()
   fmt.Println("get unlocked main")
   time.Sleep(time.Second)
}

上面的示例中,我们在for循环之前开始加锁,然后在每一次循环中创建一个协程,并对其加锁,但是由于之前已经加锁了,所以这个for循环中的加锁会陷入阻塞直到main中的锁被解锁, time.Sleep(time.Second) 是为了能让系统有足够的时间运行for循环,输出结果如下:

start lock main
get locked main
start lock  3
start lock  1
start lock  2
Unlock the lock main
get unlocked main
get locked  3

最终在main解锁后,三个协程会重新抢夺互斥锁权,最终协程3获胜。

互斥锁锁定操作的逆操作并不会导致协程阻塞,但是有可能导致引发一个无法恢复的运行时的panic,比如对一个未锁定的互斥锁进行解锁时就会发生panic。避免这种情况的最有效方式就是使用defer。

我们知道如果遇到panic,可以使用recover方法进行恢复,但是如果对重复解锁互斥锁引发的panic却是徒劳的(Go 1.8及以后)。

func main()  {
   defer func() {
      fmt.Println("Try to recover the panic")
      if p := recover();p!=nil{
         fmt.Println("recover the panic : ",p)
      }
   }()
   var mutex sync.Mutex
   fmt.Println("start lock")
   mutex.Lock()
   fmt.Println("get locked")
   fmt.Println("unlock lock")
   mutex.Unlock()
   fmt.Println("lock is unlocked")
   fmt.Println("unlock lock again")
   mutex.Unlock()
}

以上代码试图对重复解锁引发的panic进行recover,但是我们发现操作失败,输出结果:

start lock
get locked
fatal error: sync: unlock of unlocked mutex
unlock lock
lock is unlocked
unlock lock again

goroutine 1 [running]:
runtime.throw(0x4c2b46, 0x1e)
    C:/Go/src/runtime/panic.go:619 +0x88 fp=0xc04207dea8 sp=0xc04207de88 pc=0x428978
sync.throw(0x4c2b46, 0x1e)
    C:/Go/src/runtime/panic.go:608 +0x3c fp=0xc04207dec8 sp=0xc04207dea8 pc=0x4288dc
sync.(*Mutex).Unlock(0xc042060080)
    C:/Go/src/sync/mutex.go:184 +0xc9 fp=0xc04207def0 sp=0xc04207dec8 pc=0x456b59
main.main()
    D:/GoDemo/src/MyGo/Demo_04.go:23 +0x1dd fp=0xc04207df88 sp=0xc04207def0 pc=0x48ca9d
runtime.main()
    C:/Go/src/runtime/proc.go:198 +0x20e fp=0xc04207dfe0 sp=0xc04207df88 pc=0x42a21e
runtime.goexit()
    C:/Go/src/runtime/asm_amd64.s:2361 +0x1 fp=0xc04207dfe8 sp=0xc04207dfe0 pc=0x44f791

虽然互斥锁可以被多个协程共享,但还是建议将对同一个互斥锁的加锁解锁操作放在同一个层次的代码中。

2 读写锁
读写锁是针对读写操作的互斥锁,可以分别针对读操作与写操作进行锁定和解锁操作 。
读写锁的访问控制规则如下:
①多个写操作之间是互斥的
②写操作与读操作之间也是互斥的
③多个读操作之间不是互斥的
在这样的控制规则下,读写锁可以大大降低性能损耗。

由标准库代码包中sync中的RWMutex结构体表示

// RWMutex是一个读/写互斥锁,可以由任意数量的读操作或单个写操作持有。
// RWMutex的零值是未锁定的互斥锁。
//首次使用后,不得复制RWMutex。
//如果goroutine持有RWMutex进行读取而另一个goroutine可能会调用Lock,那么在释放初始读锁之前,goroutine不应该期望能够获取读锁定。 
//特别是,这种禁止递归读锁定。 这是为了确保锁最终变得可用; 阻止的锁定会阻止新读操作获取锁定。
type RWMutex struct {
   w           Mutex  //如果有待处理的写操作就持有
   writerSem   uint32 // 写操作等待读操作完成的信号量
   readerSem   uint32 //读操作等待写操作完成的信号量
   readerCount int32  // 待处理的读操作数量
   readerWait  int32  // number of departing readers
}

sync中的RWMutex有以下几种方法:

//对读操作的锁定
func (rw *RWMutex) RLock()
//对读操作的解锁
func (rw *RWMutex) RUnlock()
//对写操作的锁定
func (rw *RWMutex) Lock()
//对写操作的解锁
func (rw *RWMutex) Unlock()

//返回一个实现了sync.Locker接口类型的值,实际上是回调rw.RLock and rw.RUnlock.
func (rw *RWMutex) RLocker() Locker

Unlock会试图唤醒所有因欲进行读锁定而被阻塞的协程,而 RUnlock 只会在已无任何读锁定的情况下,试图唤醒一个因欲进行写锁定而被阻塞的协程。若对一个未被写锁定的读写锁进行写解锁,就会引发一个不可恢复的panic,同理对一个未被读锁定的读写锁进行读写锁也会如此。

由于读写锁控制下的多个读操作之间不是互斥的,因此对于读解锁更容易被忽视。对于同一个读写锁,添加多少个读锁定,就必要有等量的读解锁,这样才能其他协程有机会进行操作。

func main() {
   var rwm sync.RWMutex
   for i := 0; i < 3; i++ {
      go func(i int) {
         fmt.Println("try to lock read ", i)
         rwm.RLock()
         fmt.Println("get locked ", i)
         time.Sleep(time.Second *2)
         fmt.Println("try to unlock for reading ", i)
         rwm.RUnlock()
         fmt.Println("unlocked for reading ", i)
      }(i)
   }
   time.Sleep(time.Millisecond * 1000)
   fmt.Println("try to lock for writing")
   rwm.Lock()
   fmt.Println("locked for writing")
}

上面的示例创建了三个协程用于对读写锁的读锁定与读解锁操作。在 rwm.Lock()种会对main中协程进行写锁定,但是for循环中的读解锁尚未完成,因此会造成mian中的协程阻塞。当for循环中的读解锁操作都完成后就会试图唤醒main中阻塞的协程,main中的写锁定才会完成。输出结果如下

try to lock read  0
get locked  0
try to lock read  2
get locked  2
try to lock read  1
get locked  1
try to lock for writing
try to unlock for reading  0
unlocked for reading  0
try to unlock for reading  2
unlocked for reading  2
try to unlock for reading  1
unlocked for reading  1
locked for writing
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容