GO 互斥锁sync.Mutex (1) - Go语言中文社区

GO 互斥锁sync.Mutex (1)


在去学习go语言锁机制的时候,我会问自己几个问题:

1.锁是什么 ,为什么要用锁?
2.都有哪些锁怎么用?
3.出现问题了怎么办?
4.如何抉择和调优?

锁是什么 ,为什么要用锁?

在解释什么是锁之前,我们先了解下什么样的场景需要使用到锁,锁用于解决什么问题。
Go 语言宣扬的“用通讯的方式共享数据”,用句白话说就是可以用channel来实现锁的功能.但是通过共享数据的方式来传递信息和协调线程运行的做法其实更加主流。go依然提供了很多传统的并发控制api,这些东西基本都在sync包中。

竞态条件

用共享数据的方式来传递信息势必会面临一个问题,一旦数据被多个线程共享,那么就很可能会产生争用和冲突的情况。这种情况也被称为竞态条件(race condition)。

数据的一致性

竞态条件往往会破会共享数据的一致性。
共享数据的一致性代表着某种约定,即:多个线程对共享数据的操作总是可以达到它们各自预期的效果。
如果这个一致性得不到保证,那么将会影响到一些线程中代码和流程的正确执行,甚至会造成某种不可预知的错误。这种错误一般都很难发现和定位,排查起来的成本也是非常高的,所以一定要尽量避免。

例子:

举个例子,同时有多个线程连续向同一个缓冲区写入数据块,如果没有一个机制去协调这些线程的写入操作的话,那么被写入的数据块就很可能会出现错乱。比如,在线程A还没有写完一个数据块的时候,线程B就开始写入另外一个数据块了。显然,这两个数据块中的数据会被混在一起,并且已经很难分清了。因此,在这种情况下,我们就需要采取一些措施来协调它们对缓冲区的修改。这通常就会涉及同步。
概括来讲,同步的用途有两个,一个是避免多个线程在同一时刻操作同一个数据块,另一个是协调多个线程,以避免它们在同一时刻执行同一个代码块。

demo1

var i []int
var lock *sync.Mutex

func init() {
    lock = &sync.Mutex{}
    i = make([]int, 1)
}

func A() {
    update("A", 1)
}

func B() {
    update("B", 2)
}

func update(name string, v int) {
    //lock.Lock()
    //defer lock.Unlock()
    i[0] = v
    time.Sleep(1000)
    fmt.Printf("%s-%dn", name, i[0])
}

func MutexTest1() {
    for i := 0; i < 10; i++ {
        go A()
    }
    for i := 0; i < 10; i++ {
        go B()
    }
}

期望输出的结果是:
A-1
B-2
实际输出的结果:
A-1, A-2 ,B-1, B-2 都是有可能。
这显然与他们各自的预期结果是不符合。

临界区(critical section)

上述例子中的‘i’就是一个共享资源,一个线程在想要访问某一个共享资源的时候,需要先申请对该资源的访问权限,并且只有在申请成功之后,访问才能真正开始。而当线程对共享资源的访问结束时,它还必须归还对该资源的访问权限,若要再次访问仍需申请。你可以把这里所说的访问权限想象成一块令牌,线程一旦拿到了令牌,就可以进入指定的区域,从而访问到资源,而一旦线程要离开这个区域了,就需要把令牌还回去,绝不能把令牌带走。如果针对某个共享资源的访问令牌只有一块,那么在同一时刻,就最多只能有一个线程进入到那个区域,并访问到该资源。这时,我们可以说,多个并发运行的线程对这个共享资源的访问是完全串行的。只要一个代码片段需要实现对共享资源的串行化访问,就可以被视为一个临界区(critical section),也就是我刚刚说的,由于要访问到资源而必须进入的那个区域。如果针对同一个共享资源,这样的代码片段有多个,那么它们就可以被称为相关临界区

同步工具

临界区总是需要受到保护的,否则就会产生竞态条件。施加保护的重要手段之一,就是使用实现了某种同步机制的工具,也称为同步工具。在Go语言中,可供我们选择的同步工具并不少。其中,最重要且最常用的同步工具当属互斥量(mutual exclusion,简称mutex)。sync包中的Mutex就是与其对应的类型,该类型的值可以被称为互斥量或者互斥锁。

所以说锁其实是一种同步工具,能消除竞态条件,保护共享数据的数据的一致性。

都有哪些锁,怎么用?

go语言的sync包下实现了两种锁。sync.Mutex (互斥锁)和 sync.RWMutex(读写锁)。
sync.Cond有些地方称为条件变量,有些称为条件锁。这个看你怎么理解了。

sync.Mutex (互斥锁mutual exclusion,简称mutex)

如demo1

func update(name string, v int) {
    lock.Lock()
    defer lock.Unlock()
    i[0] = v
    time.Sleep(1000)
    fmt.Printf("%s-%dn", name, i[0])
}

一个互斥锁可以被用来保护一个临界区或者一组相关临界区。我们可以通过它来保证,在同一时刻只有一个goroutine处于该临界区之内。为了兑现这个保证,每当有goroutine想进入临界区时,都需要先对它进行锁定,并且,每个goroutine离开临界区时,都要及时地对它进行解锁。
锁定操作可以通过调用互斥锁的Lock方法实现,而解锁操作可以调用互斥锁的Unlock方法。

使用互斥锁的注意事项如下:

1.不要重复锁定互斥锁;(避免死锁)
2.不要忘记解锁互斥锁,必要时使用defer语句;(防止忘记解锁)
3.不要对尚未锁定或者已解锁的互斥锁解锁;(会panic)
4.不要在多个函数之间直接传递互斥锁。(顺序和锁的次数有错乱的可能造成死锁)

死锁

所谓的死锁,指的就是当前程序中的主goroutine,以及我们启用的那些goroutine都已经被阻塞。这些goroutine可以被统称为用户级的goroutine。这就相当于整个程序都已经停滞不前了。Go语言运行时系统是不允许这种情况出现的(目前还没接触过允许死锁的语言),只要它发现所有的用户级goroutine都处于等待状态,就会自行抛出一个带有如下信息的panic:
fatal error: all goroutines are asleep - deadlock!
注意,这种由Go语言运行时系统自行抛出的panic都属于致命错误,都是无法被恢复的,调用recover函数对它们起不到任何作用。也就是说,一旦产生死锁,程序必然崩溃。

版权声明:本文来源简书,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://www.jianshu.com/p/b21fef278ba1
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-01-12 12:54:04
  • 阅读 ( 1051 )
  • 分类:Go

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢