Go基础系列:互斥锁Mutex和读写锁RWMutex用法详述(一)

ync.Mutex

Go中使用sync.Mutex类型实现mutex(排他锁、互斥锁)。在源代码的sync/mutex.go文件中,有以下定义:web

// A Mutex is a mutual exclusion lock.
// The zero value for a Mutex is an unlocked mutex.
//
// A Mutex must not be copied after first use.
type Mutex struct {
    state int32
    sema uint32
}

这没有任何非凡的地方。和mutex相关的全部事情都是经过sync.Mutex类型的两个方法sync.Lock()和sync.Unlock()函数来完成的,前者用于获取sync.Mutex锁,后者用于释放sync.Mutex锁。sync.Mutex一旦被锁住,其它的Lock()操做就没法再获取它的锁,只有经过Unlock()释放锁以后才能经过Lock()继续获取锁。安全

也就是说,已有的锁会致使其它申请Lock()操做的goroutine被阻塞,且只有在Unlock()的时候才会解除阻塞并发

另外须要注意,sync.Mutex不区分读写锁,只有Lock()与Lock()之间才会致使阻塞的状况,若是在一个地方Lock(),在另外一个地方不Lock()而是直接修改或访问共享数据,这对于sync.Mutex类型来讲是容许的,由于mutex不会和goroutine进行关联。若是想要区分读、写锁,可使用sync.RWMutex类型,见后文。函数

在Lock()和Unlock()之间的代码段称为资源的临界区(critical section),在这一区间内的代码是严格被Lock()保护的,是线程安全的,任何一个时间点都只能有一个goroutine执行这段区间的代码ui

如下是使用sync.Mutex的一个示例,稍后是很是详细的分析过程。spa

package main

import (
    "fmt"
    "sync"
    "time"
)

// 共享变量
var (
    m  sync.Mutex
    v1 int
)

// 修改共享变量
// 在Lock()和Unlock()之间的代码部分是临界区
func change(i int) {
    m.Lock()
    time.Sleep(time.Second)
    v1 = v1 + 1
    if v1%10 == 0 {
        v1 = v1 - 10*i
    }
    m.Unlock()
}

// 访问共享变量
// 在Lock()和Unlock()之间的代码部分是是临界区
func read() int {
    m.Lock()
    a := v1
    m.Unlock()
    return a
}

func main() {
    var numGR = 21
    var wg sync.WaitGroup

    fmt.Printf("%d", read())

    // 循环建立numGR个goroutine
    // 每一个goroutine都执行change()、read()
    // 每一个change()和read()都会持有锁
    for i := 0; i < numGR; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            change(i)
            fmt.Printf(" -> %d", read())
        }(i)
    }

    wg.Wait()
}

第一次执行结果:线程

0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> -100 -> -99
-> -98 -> -97 -> -96 -> -95 -> -94 -> -93 -> -92 -> -91 -> -260 -> -259

第二次执行结果:注意其中的-74和-72之间跨了一个数code

0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> -80 -> -79 
-> -78 -> -77 -> -76 -> -75 -> -74 -> -72 -> -71 -> -230 -> -229 -> -229

上面的示例中,change()、read()都会申请锁,并在准备执行完函数时释放锁,它们如何修改数据、访问数据本文很少作解释。须要详细解释的是main()中的for循环部分。资源

在for循环中,会不断激活新的goroutine(共21个)执行匿名函数,在每一个匿名函数中都会执行change()和read(),意味着每一个goroutine都会申请两次锁、释放两次锁,且for循环中没有任何Sleep延迟,这21个goroutine几乎是一瞬间同时激活的。it

但因为change()和read()中都申请锁,对于这21个goroutine将要分别执行的42个critical section,Lock()保证了在某一时间点只有其中一个goroutine能访问其中一个critical section。当释放了一个critical section,其它的Lock()将争夺互斥锁,也就是所谓的竞争现象(race condition)。由于竞争的存在,这42个critical section被访问的顺序是随机的,彻底没法保证哪一个critical section先被访问。

对于前9个被调度到的goroutine,不管是哪一个goroutine取得这9个change(i)中的critical section,都只是对共享变量v1作加1运算,但当第10个goroutine被调度时,因为v1加1以后获得10,它知足if条件,会执行v1 = v1 - i*10,但这个i多是任意0到numGR之间的值(由于没法保证并发的goroutine的调度顺序),这使得v1的值从第10个goroutine开始出现随机性。但从第10到第19个goroutine被调度的过程当中,也只是对共享变量v1作加1运算,这些值是能够根据第10个数推断出来的,到第20个goroutine,又再次随机。依此类推。

此外,每一个goroutine中的read()也都会参与锁竞争,因此并不能保证每次change(i)以后会随之执行到read(),可能goroutine 1的change()执行完后,会跳转到goroutine 3的change()上,这样一来,goroutine 1的read()就没法读取到goroutine 1所修改的v1值,而是访问到其它goroutine中修改后的值。因此,前面的第二次执行结果中出现了一次数据跨越。只不过执行完change()后当即执行read()的概率比较大,因此多数时候输出的数据都是连续的。

总而言之,Mutex保证了每一个critical section安全,某一时间点只有一个goroutine访问到这部分,但也所以而出现了随机性

若是Lock()后忘记了Unlock(),将会永久阻塞而出现死锁。若是

 

适合sync.Mutex的数据类型

其实,对于内置类型的共享变量来讲,使用sync.Mutex和Lock()、Unlock()来保护也是不合理的,由于它们自身不包含Mute

相关文章
相关标签/搜索