有时候,我们在go代码编写中会出现多个goroutine同时操作一个临界区(也就是一个资源),这种情况会发生竞态问题。
会出现竞态问题的代码如下:
Go1var x int642 3var sg sync.WaitGroup4 5func add () {6 7 for i:=0;i++ {8 x= x+19 } 10 wg.Done()11 12}13 14func main (){15wg.Add(2)16go add()17go add()18wg.Wait()19fmt.Println(x)20}
上面我们使用了开启了两个协程调用add对x进行累加,这两个协程在修改和访问x变量时会出现数据竞争的问题,导致最后结果和预期是不符的
这种情况下我们可以使用加锁的方式控制go协程对共享资源进行操作以保证不会出现竞态问题。
互斥锁
go提供了sync包中的Mutex类型对互斥锁进行实现。他能保证同时只有一个goroutine可以访问共享资源。互斥锁能保证在同一时间,对资源的写入和读取操作有且只有一个,并且多个协程在等待同一个锁的时候,唤醒协程的策略是随机的。
ps:互斥锁的性能是比较低的,go提供的互斥锁的类型是经过优化的,在绝大多数情况下是比自己写锁是要好的。
现在使用互斥锁来解决上面的问题
Go1var x int642 3var sg sync.WaitGroup4 5var lock sync.Mutex6 7func add () {8 9 for i:=0;i++ {10 lock.Lock()11 x= x+112 lock.Unlock13 } 14 wg.Done()15 16}17 18func main (){19wg.Add(2)20go add()21go add()22wg.Wait()23fmt.Println(x)24}
读写互斥锁
互斥锁是完全互斥的,这个就导致在实际场景下(读多写少),我们去并发的读取一个资源,加互斥锁的话会导致性能的急剧下降,这种情况下我们可以使用读写锁。sync包中也提供了一个读写锁的实现,就是sync包中的RWMutex类型。
读写锁分为两种锁,一种是读锁,一种是写锁。在协程获取到资源的读锁后,其他协程如果获取读锁,将会继续获得读锁,而获取写锁的话将会等待所有读锁释放后才能获取到写锁。同理,当一个协程获取到写锁时,所有获取读锁和写锁的协程都将等待写锁释放后才能获取对应的写锁和读锁。
读写锁示例:
Go1var (2 3 x int644 5 wg sync.WaitGroup6 7 lock sync.Mutex8 9 rwlock sync.RWMutex10 11)12 13 14 15func write() {16 17 // lock.Lock() // 加互斥锁18 19 rwlock.Lock() // 加写锁20 21 x = x + 122 23 time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒24 25 rwlock.Unlock() // 解写锁26 27 // lock.Unlock() // 解互斥锁28 29 wg.Done()30 31}32 33 34 35func read() {36 37 // lock.Lock() // 加互斥锁38 39 rwlock.RLock() // 加读锁40 41 time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒42 43 rwlock.RUnlock() // 解读锁44 45 // lock.Unlock() // 解互斥锁46 47 wg.Done()48 49}50 51 52 53func main() {54 55 start := time.Now()56 57 for i := 0; i < 10; i++ {58 59 wg.Add(1)60 61 go write()62 63 }64 65 66 67 for i := 0; i < 1000; i++ {68 69 wg.Add(1)70 71 go read()72 73 }74 75 76 77 wg.Wait()78 79 end := time.Now()80 81 fmt.Println(end.Sub(start))82 83}