GO标准库巡礼-sync

在go中sync负责提供同步原语如互斥锁等。任何属于该包类型的对象都不应该被复制(只能passed by pointer)

sync.Mutex

sync.Mutex是最常用的同步原语。其作用在于对共享资源的互斥访问。

常用的使用范式
mutex := &sync.Mutex{}

mutex.Lock()
// Update shared variable (e.g. slice, pointer on a structure, etc.)
mutex.Unlock()
使用Lock有什么注意事项?

当调用Lock的时候,如果mutex已经被上锁,那么调用Lock的goroutine会阻塞到mutex被解锁为止。

而且mutex是不可重入锁,意味着即便是同一个goroutine,如果多次对同一把锁Lock也会触发死锁

func decThenPlus(mutex *sync.Mutex,i int){
	mutex.Lock()
	defer mutex.Unlock()
	fmt.Println(i)
	plus(mutex,i-1)//死锁!
}
func plus(mutex *sync.Mutex,i int){
	mutex.Lock()
	defer mutex.Unlock()
    i++
	fmt.Println(i)
}
func main() {
	mutex:=new(sync.Mutex)
	decThenPlus(mutex,100)
}
/**
输出结果
100
fatal error: all goroutines are asleep - deadlock!
**/
使用Unlock有什么注意事项?

一般情况下,应该确保Lock和Unlock成双出现,必要使用可以使用defer mutex.Unlock()

调用Unlock的时候,如果mutex没有上锁,那么会触发运行时错误(panic)。

Lock和Unlock是可以在两个线程中分别调用的。

除此之外,实践上,应该确保“一个锁对应一个资源”,如果一个锁对应多个资源,那么会平白提高锁争用的概率。

sync.RWMutex

有些场景属于读远多于写的场景,这个时候使用mutex会大大降低性能。对此我们可以使用读写锁sync.RWMutex。读写锁与互斥锁sync.Mutex不同的地方在于,对于读操作可以共享,只有对写操作才需要互斥访问。

sync.RWMutex同样实现Lock接口。可以用LockUnlock来实现写锁,用RLock()RUnlock来实现读锁。

读锁和写锁的交互有一些值得注意的细节

当有写锁请求在等待读锁的释放,新的读锁请求会被允许吗?

不会,新的读锁请求会等到读锁释放后才能(竞争)获取。这样做是为了避免一直不断出现的读锁请求导致写锁一直阻塞(饥饿)

当已有读锁/写锁占有,读锁/写锁请求会如何?

只有在读锁占有的时候读锁请求不会被阻塞。其他的组合都会被阻塞。

读锁支持可重入读锁吗?

不支持,事实上golang在整体上认为可重入锁意味着结构有问题,所以不支持

多个goroutine在阻塞等待锁的时候,锁释放后唤醒的goroutine规则是什么?

按照等待时间来算,所以是公平的。

sync.Waitgroup

WaitGroup和其他一样都是零值即可用的工具,其作用在于同步多个线程的工作流。比方说N个goroutine在执行,这时候为了避免主goroutine完成main函数导致程序关闭,就可以用wg.Wait()来要求主goroutine等到其他goroutine的完成

代码分析:为什么这里结果和想象中不一样?
	var mutex sync.Mutex
	var sum=0
	var wg sync.WaitGroup
	for i:=0;i<100000;i++{
		go func(){
			wg.Add(1)
			defer wg.Done()
			mutex.Lock()
			sum++
			mutex.Unlock()
		}()
	}
	wg.Wait()
	fmt.Println(sum)
/**
输出结果
0
**/

这里看起来好像我们也实现了锁,也用了waitgroup来等待,但是结果却是0。其原因在于,wg.Wait()在内部计数器为0的时候就解除阻塞,而我们知道golang中的go是异步执行的。这里主goroutine快速启动了100000个goroutine后还没等goroutine运作就执行wg.Wait(),此时计数器wg.Add(1)还没有执行,导致输出0。

因此一个教训是要提前执行Add,比方说预期开启N个goroutine,那么可以在循环之前执行wg.Add(N)。或者说在启动goroutine之前先调用wg.Add(1),由于该指令是在主goroutine内,因此可以确保得到执行。

wg.Add(delta)中delta可以是负数吗?有什么作用?

可以是负数,但是如果导致内部计数器变为负数,那么Add操作触发panic。

sync.Map

参考sync.Map的实现

sync.Pool

参考sync.Pool设计与实现

sync.Once

sync.Once可以用于确保某个函数被调用且被调用一次。只有第一次调用Do时传入的函数会被执行,其他调用Do的goroutine会阻塞等到Do执行完成函数。如果有多个函数需要确保执行一次,那么就需要用多个sync.Once

由于在函数执行完成之前没有Do函数会返回,所以如果在函数内调用Do会造成死锁。

即便函数执行过程中触发panic,Do也认为已经执行完成,之后对Do的调用都会立刻返回(而不执行函数)

sync.Once常常用于要求必须执行且只能执行一次的初始化,由于传入函数没有传入参数,所以一般用闭包来传参。

值得注意的是,在Do函数中使用了经典的双重检查来提高性能

sync.Cond

sync.Cond是用来协调goroutine之间工作的,它需要与Mutex相配合。因为Wait内部会调用Unlock,所以如果在调用Wait之前上锁,那么会panic

它应用于函数要求资源符合某些条件的时候,一个最常见的例子,是生产者消费者模型中,生产者函数要求队列未满,消费者函数要求队列不空。由于涉及到多goroutine,所以需要用mutex保护资源。

假设我们不使用Cond和channel的话我们会怎么解决问题呢?我们会先拿着锁Lock,检查资源状态,如果不符合条件,则放弃锁,然后继续轮询检查直到符合条件为止。

而sync.Cond允许我们持有锁,检查资源发现不符合条件之后,调用Wait函数放弃锁并阻塞本goroutine,直到有goroutine调用Signal或者Notify通知可能符合条件。

那生产者消费者模型举例,生产者持有锁检查发现队列已满,然后可以调用Wait陷入沉睡。如果有消费者消费了队列,则消费者可以调用Signal通知,该函数会唤醒一个因为Wait而阻塞的goroutine,当然消费者也可以使用Broadcast唤醒所有在等待的线程。

值得注意的是,在调用SignalBroadcast的时候goroutine可以持有锁(未解锁),也可以不持有锁。但是从观察实现上,一般来说都是在持有锁的时候调用Signal和Broadcast。

一般来说,对于同一个资源的不同条件应该分别处理,比方说上面生产者消费者模型中应该同时有isFull和isEmpty条件,这样可以更精确的通知。如果不同的goroutine基于不同的条件却使用同一个sync.Cond(比方说生产者消费者模型中只使用用一个sync.Cond),那么要求使用Broadcast避免该通知的goroutine没有通知到。

在使用Wait的时候需要用for包裹条件,因为当wait恢复的时候c.L没有立刻上锁,所以不能保证还能符合条件

c.L.Lock()
for !condition() {
    c.Wait()
}
... make use of condition ...
c.L.Unlock()
发布了31 篇原创文章 · 获赞 32 · 访问量 731

猜你喜欢

转载自blog.csdn.net/a348752377/article/details/104978973