有时候在Go代码中可能会存在多个goroutine同时操做一个资源(临界区),这种状况会发生竞态问题(数据竞态)。Sync包主要实现了并发任务同步WaitGroup的几种方法和并发安全的互斥锁和读写锁方法,还实现了比较特殊的两个方法,一个是保持只执行一次的Once方法和线程安全的Map。golang
sync.WaitGroup内部维护着一个计数器Add(),计数器的值能够增长和减小。例如当咱们启动了N 个并发任务时,就将计数器值增长N。每一个任务完成时经过调用Done()方法将计数器减1,底层为Add(-1)。经过调用Wait()来等待并发任务执行完,当计数器值为0时,表示全部并发任务已经完成。编程
var x int64 var wg sync.WaitGroup func add() { for i := 0; i < 5000; i++ { x = x + 1 //数据竞争 } wg.Done() } func main() { wg.Add(2) go add() go add() wg.Wait() fmt.Println(x) }
上面的代码中咱们开启了两个goroutine去累加变量x的值,这两个goroutine在访问和修改x变量的时候就会存在数据竞争,致使最后的结果与期待的不符。segmentfault
互斥锁是一种经常使用的控制共享资源访问的方法,它可以保证同时只有一个goroutine能够访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。 使用互斥锁来修复上面代码的问题:安全
var x int64 var wg sync.WaitGroup var lock sync.Mutex func add() { for i := 0; i < 5000; i++ { lock.Lock() // 加锁 x = x + 1 lock.Unlock() // 解锁 } wg.Done() } func main() { wg.Add(2) go add() go add() wg.Wait() fmt.Println(x) }
使用互斥锁可以保证同一时间有且只有一个goroutine进入临界区,其余的goroutine则在等待锁;当互斥锁释放后,等待的goroutine才能够获取锁进入临界区,多个goroutine同时等待一个锁时,唤醒的策略是随机的。闭包
互斥锁是彻底互斥的,可是有不少实际的场景下是读多写少的,当咱们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync包中的RWMutex类型。并发
读写锁分为两种:读锁和写锁。当一个goroutine获取读锁以后,其余的goroutine若是是获取读锁会继续得到锁,若是是获取写锁就会等待;当一个goroutine获取写锁以后,其余的goroutine不管是获取读锁仍是写锁都会等待。app
读写锁示例:函数
var ( x int64 wg sync.WaitGroup lock sync.Mutex rwlock sync.RWMutex ) func write() { // lock.Lock() // 加互斥锁 rwlock.Lock() // 加写锁 x = x + 1 time.Sleep(10 * time.Millisecond) // 假设读操做耗时10毫秒 rwlock.Unlock() // 解写锁 // lock.Unlock() // 解互斥锁 wg.Done() } func read() { // lock.Lock() // 加互斥锁 rwlock.RLock() // 加读锁 time.Sleep(time.Millisecond) // 假设读操做耗时1毫秒 rwlock.RUnlock() // 解读锁 // lock.Unlock() // 解互斥锁 wg.Done() } func main() { start := time.Now() for i := 0; i < 10; i++ { wg.Add(1) go write() } for i := 0; i < 1000; i++ { wg.Add(1) go read() } wg.Wait() end := time.Now() fmt.Println(end.Sub(start)) }
须要注意的是读写锁很是适合读多写少的场景,若是读和写的操做差异不大,读写锁的优点就发挥不出来。高并发
说在前面的话:这是一个进阶知识点。性能
在编程的不少场景下咱们须要确保某些操做在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等。
Go语言中的sync包中提供了一个针对只执行一次场景的解决方案–sync.Once。
sync.Once只有一个Do方法,其签名以下:
func (o *Once) Do(f func()) {}
注意:若是要执行的函数f须要传递参数就须要搭配闭包来使用。
Go语言中内置的map不是并发安全的。请看下面的示例:
var m = make(map[string]int) func get(key string) int { return m[key] } func set(key string, value int) { m[key] = value } func main() { wg := sync.WaitGroup{} for i := 0; i < 20; i++ { wg.Add(1) go func(n int) { key := strconv.Itoa(n) set(key, n) fmt.Printf("k=:%v,v:=%v\n", key, get(key)) wg.Done() }(i) } wg.Wait() }
上面的代码开启少许几个goroutine的时候可能没什么问题,当并发多了以后执行上面的代码就会报fatal error: concurrent map writes错误。
像这种场景下就须要为map加锁来保证并发的安全性了,Go语言的sync包中提供了一个开箱即用的并发安全版map–sync.Map。开箱即用表示不用像内置的map同样使用make函数初始化就能直接使用。同时sync.Map内置了诸如Store、Load、LoadOrStore、Delete、Range等操做方法。
var m = sync.Map{} func main() { wg := sync.WaitGroup{} for i := 0; i < 20; i++ { wg.Add(1) go func(n int) { key := strconv.Itoa(n) m.Store(key, n) value, _ := m.Load(key) fmt.Printf("k=:%v,v:=%v\n", key, value) wg.Done() }(i) } wg.Wait() }
代码中的加锁操做由于涉及内核态的上下文切换会比较耗时、代价比较高。针对基本数据类型咱们还可使用原子操做来保证并发安全,由于原子操做是Go语言提供的方法它在用户态就能够完成,所以性能比加锁操做更好。Go语言中原子操做由内置的标准库sync/atomic提供。
示例:
package main import ( "fmt" "sync" "sync/atomic" "time" ) var x int64 var l sync.Mutex var wg sync.WaitGroup // 普通版加函数 func add() { // x = x + 1 x++ // 等价于上面的操做 wg.Done() } // 互斥锁版加函数 func mutexAdd() { l.Lock() x++ l.Unlock() wg.Done() } // 原子操做版加函数 func atomicAdd() { atomic.AddInt64(&x, 1) wg.Done() } func main() { start := time.Now() for i := 0; i < 10000; i++ { wg.Add(1) //go add() // 普通版add函数 不是并发安全的 //go mutexAdd() // 加锁版add函数 是并发安全的,可是加锁性能开销大 go atomicAdd() // 原子操做版add函数 是并发安全,性能优于加锁版 } wg.Wait() end := time.Now() fmt.Println(x) fmt.Println(end.Sub(start)) }