Go并发编程
Go语言 竞争状态
有并发,就有资源竞争,如果两个或者多个 goroutine 在没有相互同步的情况下,访问某个共享的资源,比如同时对该资源进行读写时,就会处于相互竞争的状态,这就是并发中的资源竞争。 对于同一个资源的读写必须是原子化的,也就是说,同一时间只能允许有一个 goroutine 对共享资源进行读写操作。 ## 并发安全和锁 #### 互斥锁 互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。 使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其他的goroutine则在等待锁;当互斥锁释放后,等待的goroutine才可以获取锁进入临界区,多个goroutine同时等待一个锁时,唤醒的策略是随机的。 ```go 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) } ``` #### 读写互斥锁 互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync包中的RWMutex类型。 读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。 读写锁非常适合读多写少的场景,如果读和写的操作差别不大,读写锁的优势就发挥不出来 ```go 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)) } ``` ## 原子操作(atomic包) 代码中的加锁操作因为涉及内核态的上下文切换会比较耗时、代价比较高。针对基本数据类型我们还可以使用原子操作来保证并发安全,因为原子操作是Go语言提供的方法它在用户态就可以完成,因此性能比加锁操作更好。Go语言中原子操作由内置的标准库sync/atomic提供。 atomic包提供了底层的原子级内存操作,对于同步算法的实现很有用。这些函数必须谨慎地保证正确使用。除了某些特殊的底层应用,使用通道或者sync包的函数/类型实现同步更好。 - atomic包 - 读取操作 - func LoadInt32(addr int32) (val int32) - func LoadInt64(addr int64) (val int64) - func LoadUint32(addruint32) (val uint32) - func LoadUint64(addruint64) (val uint64) - func LoadUintptr(addruintptr) (val uintptr) - func LoadPointer(addrunsafe.Pointer) (val unsafe.Pointer) - 写入操作 - func StoreInt32(addr *int32, val int32) - func StoreInt64(addr *int64, val int64) - func StoreUint32(addr *uint32, val uint32) - func StoreUint64(addr *uint64, val uint64) - func StoreUintptr(addr *uintptr, val uintptr) - func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer) - 修改操作 - func AddInt32(addr *int32, delta int32) (new int32) - func AddInt64(addr *int64, delta int64) (new int64) - func AddUint32(addr *uint32, delta uint32) (new uint32) - func AddUint64(addr *uint64, delta uint64) (new uint64) - func AddUintptr(addr *uintptr, delta uintptr) (new uintptr) - 交换操作 - func SwapInt32(addr *int32, new int32) (old int32) - func SwapInt64(addr *int64, new int64) (old int64) - func SwapUint32(addr *uint32, new uint32) (old uint32) - func SwapUint64(addr *uint64, new uint64) (old uint64) - func SwapUintptr(addr *uintptr, new uintptr) (old uintptr) - func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer) - 比较并交换操作 - func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool) - func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool) - func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool) - func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool) - func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool) - func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool) ```go 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)) } ``` ## go build -race 共享资源竞争的问题,非常复杂,并且难以察觉, Go提供了一个工具:go build -race 命令。在项目目录下执行这个命令,生成一个可以执行文件,然后再运行这个可执行文件,就可以看到打印出的检测信息。 在go build命令中多加了一个-race 标志,这样生成的可执行程序就自带了检测资源竞争的功能。
顶部
收展
底部
[TOC]
目录
Go语言 并发
Go语言 goroutine(轻量级线程)
Go语言 通道Channel
Go语言 竞争状态
Go语言 死锁、活锁和饥饿
Go语言 select
相关推荐
Go基础
Go数据类型
Go函数
Go面向对象
Go数据操作