如果你对通过内存访问同步处理并发的语言很熟悉,那么你可能会立即明白Mutex的使用方法。如果你没有这样的经验,没关系,Mutex很容易理解。Mutex代表”mutual exclusion(互斥)”。互斥提供了一种并发安全的方式来表示对共享资源访问的独占。下面是一个简单的两个goroutine,它们试图增加和减少一个公共值;,并使用Mutex来同步访问:

var count int
var lock sync.Mutex

increment := func() {
    lock.Lock() // 1
    defer lock.Unlock() // 2
    count++
    fmt.Printf("Incrementing: %d\n", count)
}

decrement := func() {
    lock.Lock() // 1
    defer lock.Unlock() // 2
    count--
    fmt.Printf("Decrementing: %d\n", count)
}

// Increment
var arithmetic sync.WaitGroup
for i := 0; i <= 5; i++ {
    arithmetic.Add(1)
    go func() {
        defer arithmetic.Done()
        increment()
    }()

}

// Decrement
for i := 0; i <= 5; i++ {
    arithmetic.Add(1)
    go func() {
        defer arithmetic.Done()
        decrement()
    }()
}

arithmetic.Wait()
fmt.Println("Arithmetic complete.")
  1. 在这里,我们要求独占使用关键部分 - 在这种情况下,count变量由互斥锁保护。
  2. 这里表明我们已经完成了对共享部分的锁定。

这会输出:

Decrementing: -1
Incrementing: 0
Decrementing: -1
Incrementing: 0
Decrementing: -1
Decrementing: -2
Decrementing: -3
Incrementing: -2
Decrementing: -3
Incrementing: -2
Incrementing: -1
Incrementing: 0 Arithmetic complete.

你会注意到我们总是使用defer在延迟声明中调用解锁。 使用互斥锁时,这是一个非常常见的习惯用法,以确保调用始终执行,即使在发生恐慌时也是如此。否则一旦未能解除锁定,可能会导致你的程序陷入死锁。

被锁定部分是程序的性能瓶颈,进入和退出锁定的成本有点高,因此人们通常尽量减少锁定涉及的范围。

可能在多个并发进程之间共享的内存并不是都要读取和写入,出于这样的考虑,你可以使用另一个类型的互斥锁:sync.RWMutex。

sync.RWMutex与Mutex在概念上是一样的:它保护对内存的访问;不过,RWMutex可以给你更多地控制方式。 你可以请求锁定进行读取,在这种情况下,你将被授予读取权限,除非锁定正在进行写入操作。 这意味着,只要没有别的东西占用写操作,任意数量的读取者就可以进行读取操作。 下面是一个演示生产者的示例:

producer := func(wg *sync.WaitGroup, l sync.Locker) { //1
    defer wg.Done()
    for i := 5; i > 0; i-- {
        l.Lock()
        l.Unlock()
        time.Sleep(1) //2
    }
}

observer := func(wg *sync.WaitGroup, l sync.Locker) {
    defer wg.Done()
    l.Lock()
    defer l.Unlock()
}

test := func(count int, mutex, rwMutex sync.Locker) time.Duration {
    var wg sync.WaitGroup
    wg.Add(count + 1)
    beginTestTime := time.Now()
    go producer(&wg, mutex)
    for i := count; i > 0; i-- {
        go observer(&wg, rwMutex)
    }

    wg.Wait()
    return time.Since(beginTestTime)
}

tw := tabwriter.NewWriter(os.Stdout, 0, 1, 2, ' ', 0)
defer tw.Flush()

var m sync.RWMutex
fmt.Fprintf(tw, "Readers\tRWMutext\tMutex\n")
for i := 0; i < 20; i++ {
    count := int(math.Pow(2, float64(i)))
    fmt.Fprintf(
        tw, "%d\t%v\t%v\n", count,
        test(count, &m, m.RLocker()), test(count, &m, &m),
    )
}
  1. producer函数的第二个参数是类型sync.Locker。 该接口有两种方法,锁定和解锁,互斥和RWMutex类型都适用。
  2. 在这里,我们让producer休眠一秒钟,使其不那么活跃。

这会输出:

Readers  RWMutext  Mutex
1        5ms       5ms
2        5ms       5ms
4        5ms       5ms
8        5ms       5ms
16       5ms       5ms
32       5ms       5ms
64       5ms       5ms
128      5ms       5ms
256      5ms       5ms
512      5ms       5ms
1024     5ms       5ms
2048     5ms       5ms
4096     6ms       7ms
8192     8ms       8ms
16384    7ms       8ms
32768    9ms       11ms
65536    12ms      15ms
131072   29ms      31ms
262144   61ms      68ms
524288   121ms     137ms

你可以通过这个例子看到,RWMutext在大量级上相对于Mutex是有性能优势的,不过这同样取决于你在锁住的部分做了什么。通常建议在逻辑上合理的情况下使用RWMutex而不是Mutex。

最后编辑: kuteng  文档更新时间: 2021-01-02 17:30   作者:kuteng