2017年12月

关于 Golang 中的锁

为什么需要锁

在进行并发编程的时候,通过共享内存的方式进行通信,这时可能就会导致资源的竞争,最终导致出现数据错误。

比如下面这段demo程序,两个goroutine同时运行,循环执行a自增1的操作100000次,正常情况下,a的最终值我们的期望是200000,但结果不是!输出的值会比200000小,这是因为两个goroutine通过共享内存(也就是变量a)的方式进行通信,当goroutine1取到a准备做自增,比如这时a的值是10,这时goroutine2刚好完成了一次自增(a变成11),但goroutine1并不能感知到goroutine2中的a值的变化。所以goroutine1自增完,a的值还是11。导致最终结果“错误”。我们要避免这种情况的话,就需要使用锁。

a := 0
// goroutine1
go func() {
   for i := 0; i < 100000; i++ {
      a += 1
   }
}()
// goroutine2
go func() {
   for i := 0; i < 100000; i++ {
      a += 1
   }
}()
time.Sleep(time.Second) // 等待goroutine执行完成
fmt.Println(a)

Golang 中是如何实现锁的

Golang 中有两种锁,分别是互斥锁(sync.Mutex)和读写锁(sync.RWMutex),我们分别来看。

互斥锁(sync.Mutex)

互斥锁比较简单,当我们对一个goroutine上了互斥锁之后,其他的goroutine就只能乖乖的等待这把锁解锁,使用方法如下:

var mutex sync.Mutex
mutex.Lock() // 加锁
// ...
mutex.UnLock() // 解锁

我们要解决上面demo的问题,就可以使用互斥锁,代码如下,这时输出结果就正确了。

var mutex sync.Mutex
a := 0
// goroutine1
go func() {
   for i := 0; i < 100000; i++ {
      mutex.Lock() // 上互斥锁
      a += 1
      mutex.Unlock() // 解互斥锁
   }
}()
// goroutine2
go func() {
   for i := 0; i < 100000; i++ {
      mutex.Lock()
      a += 1
      mutex.Unlock()
   }
}()
time.Sleep(time.Second)
fmt.Println(a)

读写锁(sync.RWMutex)

通常情况下,互斥锁能满足很多的应用场景了,不过,互斥锁比较耗费资源,会拖慢程序的执行效率。在一些读取操作大大的多于写入操作的场景下,互斥锁就不太适合了,这时我们就需要读写锁。读写锁的使用方式如下:

var rwmutex sync.RWMutex
rwmutex.Lock() // 加写锁
rwmutex.Unlock() // 解写锁
rwmutex.RLock() // 加读锁
rwmutex.RUnlock() // 解读锁

看如下的demo,我模拟了读多写少的场景,mutex函数使用互斥锁,rwmutex函数使用读写锁,其他的读写操作都一样。最终程序的输出了两个函数的执行时间:

mutex time: 109.784714ms
rwmutex time: 36.494956ms

可以看出读多写少的场景下,读写锁比互斥锁的效率更高。

package main

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

func main() {
   mutex()
   rwmutex()
}

// 使用互斥锁
func mutex() {
   var mutex sync.Mutex
   var wg sync.WaitGroup

   bt := time.Now()
   a := 0
   b := 0
   wg.Add(9)
   // 读操作
   for i := 1; i < 10; i++ {
      go func() {
         for i := 0; i < 100000; i++ {
            mutex.Lock()
            b = a
            mutex.Unlock()
         }
         wg.Done()
      }()
   }

   wg.Add(1)
   // 写操作
   go func() {
      for i := 0; i < 100; i++ {
         mutex.Lock()
         a += 1
         mutex.Unlock()
      }
      wg.Done()
   }()

   wg.Wait()
   et := time.Now().Sub(bt)
   fmt.Println("mutex time:", et.String())
}

// 使用读写锁
func rwmutex() {
   var rwmutex sync.RWMutex
   var wg sync.WaitGroup

   bt := time.Now()
   a := 0
   b := 0
   wg.Add(9)
   // 读操作
   for i := 1; i < 10; i++ {
      go func() {
         for i := 0; i < 100000; i++ {
            // 上读锁,多个goroutine可同时获取读锁,这里不会阻塞
            // 但读锁会阻塞其他写锁,直到读锁释放
            rwmutex.RLock()
            b = a
            rwmutex.RUnlock()
         }
         wg.Done()
      }()
   }

   wg.Add(1)
   // 写操作
   go func() {
      for i := 0; i < 100; i++ {
         // 上写锁,会阻塞其他的读锁和写锁,直到写锁释放
         rwmutex.Lock()
         a += 1
         rwmutex.Unlock()
      }
      wg.Done()
   }()

   wg.Wait()
   et := time.Now().Sub(bt)
   fmt.Println("rwmutex time:", et.String())
}

参考

https://golang.org/pkg/sync/
http://legendtkl.com/2016/10/26/rwmutex/