互斥锁 sync.Mutex
是传统并发程序对共享资源进行访问控制的主要手段。
var m sync.Mutex
func TestMutex(t *testing.T) {
m.Lock()
defer m.Unlock()
//TODO:执行一些操作
}
对一个未锁定的互斥锁进行解锁操作时,会导致程序异常结束,即使采用 defer
也没法拦截。
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Printf("Recovered the panic: %#v\n", err)
}
}()
mu.Lock()
fmt.Println("Locked")
mu.Unlock()
fmt.Println("Unlocked")
mu.Unlock()
fmt.Println("Unlocked")
}
sync.RWMutex
与互斥锁最大的不同,就是可以分别针对读操作和写操作进行锁定和解锁操作。读操作和写操作互斥,但多个读操作不互斥。
var rwm sync.RWMutex
//读锁
rwm.RLock()
rwm.RUnlock()
//写锁
rwm.Lock()
rwm.Unlock()
sync.NewCond(l Locker)
: 新建一个sync.Cond
变量。该函数需要一个Locker
作为必填参数,这是因为在cond.Wait()
中底层会涉及到Locker
的锁操作。cond.Wait()
: 等待被唤醒并且自动地对与该条件变量关联的那个锁进行解锁。唤醒期间会使它所在的goroutine
阻塞。cond.Signal()
: 只唤醒一个最先Wait
的goroutine
。对应的另外一个唤醒函数是Broadcast
,区别是Signal
一次只会唤醒一个goroutine
,而Broadcast
会将全部Wait
的goroutine
都唤醒。
常见的场景时:生产者消费者场景。而 producer
和 consumer
对 queue
的读写操作都由 sync.Mutex
进行并发安全的保护。
var mutex sync.Mutex
var cond = sync.NewCond(&mutex)
var queue []int
func producer() {
i := 0
for {
mutex.Lock()
queue = append(queue, i)
i++
cond.Signal()
mutex.Unlock()
}
}
func consumer(consumerName string) {
for {
mutex.Lock()
for len(queue) == 0 {
cond.Wait() //等待被唤醒。唤醒期间会解锁并阻塞 goroutine。
}
fmt.Println(consumerName, "got", queue[0])
queue = queue[1:]
mutex.Unlock()
}
}
func TestCond(t *testing.T) {
go producer()
go consumer("consumer1")
go consumer("consumer2")
for {
time.Sleep(time.Second * 1)
}
}
-
sync.Cond
不能拷贝。 -
Wait
的调用一定要放在Lock
和UnLock
中间,Wait 调用的条件检查一定要放在 for 循环中。c.L.Lock() for !condition() { c.Wait() } ... make use of condition ... c.L.Unlock()
- 原子操作即执行过程不能被中断的操作。在针对某个值的原子操作执行过程当中,CPU 绝不会再去执行其他针对该值的操作。
- 由标准库
sync/atomic
来提供对几种简单类型的值执行原子操作。这些类型包括:int32
、int64
、uint32
、uint64
、uintptr
和unsafe.Pointer
。
func TestYz(t *testing.T) {
var x int32
var wg sync.WaitGroup
for i := 0; i < 5000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt32(&x, 1)
}()
}
wg.Wait()
fmt.Println(x)
}
//有符号的增减
var x int32
atomic.AddInt32(&x, 10)
atomic.AddInt32(&x, -3)
//无符号的增减
var y uint32
atomic.AddUint32(&y, 10)
atomic.AddUint32(&y, ^uint32(10-1))
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
第一个参数的值是指向被操作值的指针值,该值的类型为*int32
。后面两个参数的类型都是int32
。调用该函数时会先判断addr
指向的值与参数old
的值是否相等。仅当此判断得到肯定的结果之后,该函数才会用参数new
代表的新值替换旧值。- 与前面讲到的锁相比,CAS 操作明显不同。它总是假设被操作值未曾改变(即与旧值相等),并一旦确定这个假设的真实性就立即进行替换。而锁则是更加谨慎的做法。锁总是先假设会有并发的操作修改被操作值,并需要使用锁将相关操作放入临界区中加以保护。可以说,使用锁的做法趋于悲观,而 CAS 操作的做法趋于乐观。
//eg:1
var z int32 = 2
atomic.CompareAndSwapInt32(&z, 2, 4)
fmt.Println(z) //4
//增加一个值
func addValue(delta int32) {
for {
v := atomic.LoadInt32(&value) //载入(读取)操作
if atomic.CompareAndSwapInt32(&value, v, v+delta) {
break
}
}
}
Store
为前缀的函数可以原子地存储某个值。
Swap
为前缀的函数可以执行原子交换操作,不关心被操作值的旧值。
atomic.Value
是一个结构体类型,它用于存储需要原子读写的值。该值一旦声明不允许复制。
var atomicVal atomic.Value
- 作为参数传入该方法的值不能为nil。
- 作为参数传入该方法的值必须与之前传入的值类型相同。
使用场景:执行仅需执行一次的任务。
var once sync.Once
var x int32
for i := 0; i < 100; i++ {
once.Do(func() {
x++
})
}
参考《 Go 并发编程实战》第5章同步