1. 并发安全
package main import ( "fmt" "sync" ) var ( sum int wg sync.WaitGroup ) func test() { for i := 0; i < 5000000; i++ { sum += 1 } wg.Done() } func main() { // 并发和安全锁 wg.Add(2) go test() go test() wg.Wait() fmt.Println(sum) }
上面的代码中我们开启了两个goroutine去累加变量x的值,这两个goroutine在访问和修改x变量的时候就会存在数据竞争,导致最后的结果与期待的不符。
2. 互斥锁
package main import ( "fmt" "sync" ) var ( sum int wg sync.WaitGroup mu sync.Mutex // 定义一个互斥锁 ) func test() { for i := 0; i < 10000000; i++ { // 互斥锁它能够保证同时只能有一个goroutine去访问共享资源 mu.Lock() sum += 1 mu.Unlock() } wg.Done() } func main() { fmt.Println(mu) // 并发和安全锁 wg.Add(2) go test() go test() wg.Wait() fmt.Println(sum) }
使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其他的goroutine则在等待锁;当互斥锁释放后,等待的goroutine才可以获取锁进入临界区,多个goroutine同时等待一个锁时,唤醒的策略是随机的。
3. 读写互斥锁
互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync包中的RWMutex类型。
读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。
package main import ( "fmt" "sync" "time" ) var ( x int wg sync.WaitGroup mu sync.Mutex // 定义一个互斥锁 rw sync.RWMutex // 定义一个读写锁,注意:只有读多写少的时候,读写锁才能发挥其优势 ) func write() { rw.Lock() x += 1 time.Sleep(10 * time.Millisecond) // 假设写入时间耗费10毫秒 rw.Unlock() wg.Done() } func read() { rw.RLock() time.Sleep(time.Millisecond) rw.RUnlock() wg.Done() } func main() { start := time.Now() for i := 0; i < 10; i++ { wg.Add(1) go write() } // 写耗时:160毫秒左右 for i := 0; i < 1000; i++ { wg.Add(1) go read() } // 读耗时:15毫秒左右 wg.Wait() end := time.Now() fmt.Println("执行时间:", end.Sub(start)) }
需要注意的是读写锁非常适合读多写少的场景,如果读和写的操作差别不大,读写锁的优势就发挥不出来。