关于后端:面试官让我用channel实现sync包里的同步锁是不是故意为难我

前言

Go语言提供了channel和sync包两种并发管制的办法,每种办法都有他们实用的场景,并不是所有并发场景都适宜利用channel的,有的时候用sync包里提供的同步原语更简略。明天这个话题纯属是为了通过用channel实现同步锁的性能来学习把握channel领有的弱小能力,并不适宜在理论中应用。而且面试中有时候就是会出一些奇奇怪怪的题考应聘者对常识的了解以及灵活运用的应变能力。

大家认真看看文章里用channel实现几种罕用的同步锁的思路,没准儿哪次面试就碰上这样的面试官了呢。

明天,我将深入探讨Go语言channelselect语句的表达能力。为了演示只用这两个原语就能够实现多少性能,我将从头开始用它们重写sync包。

sync包提供的同步原语的有哪些以及如何应用咱们曾经在之前的文章里介绍过了,所以这里不会再去介绍用channel实现的这些同步原语应该怎么用。如果对用法有疑问请回看之前的文章: Go语言sync包的利用详解。

Once

once是一个简略而弱小的原语,可确保在并行程序中一个函数仅执行一次。

channel版的Once咱们应用带有一个缓冲的通道来实现
第一次调用Do(func ())goroutine从通道中接管到值后,后续的goroutine将会被阻塞中,直到Do的参数函数执行实现后敞开通道为止。其余goroutine判断通道已敞开后将不执行任何操作并立刻返回。

`type Once chan struct{}`
`func NewOnce() Once {`
 `o := make(Once, 1)`
 `// 只容许一个goroutine接管,其余goroutine会被阻塞住`
 `o <- struct{}{}`
 `return o`
`}`
`func (o Once) Do(f func()) {`
 `_, ok := <-o`
 `if !ok {`
 `// Channel曾经被敞开`
 `// 证实f曾经被执行过了,间接return.`
 `return`
 `}`
 `// 调用f, 因为channel中只有一个值`
 `// 所以只有一个goroutine会达到这里`
 `f()`
 `// 敞开通道,这将开释所有在期待的`
 `// 以及将来会调用Do办法的goroutine`
 `close(o)`
`}`

Mutex

大小为N的信号量最多容许N个goroutine在任何给定工夫放弃其锁。互斥锁是大小为1的信号量的特例。

信号量(英语:semaphore)又称为信号标,是一个同步对象,用于放弃在0至指定最大值之间的一个计数值。当线程实现一次对该semaphore对象的期待(wait)时,该计数值减1;当线程实现一次对semaphore对象的开释(release)时,计数值加1。当计数值为0,则线程直至该semaphore对象变成signaled状态能力期待胜利。semaphore对象的计数值大于0,为signaled状态;计数值等于0,为nonsignaled状态.

咱们先用channel实现信号量的性能

`type Semaphore chan struct{}`
`func NewSemaphore(size int) Semaphore {`
 `return make(Semaphore, size)`
`}`
`func (s Semaphore) Lock() {`
 `// 只有在s还有空间的时候能力发送胜利`
 `s <- struct{}{}`
`}`
`func (s Semaphore) Unlock() {`
 `// 为其余信号量腾出空间`
 `<-s`
`}`

下面也说了互斥锁是大小为1的信号量的特例。那么在方才实现的信号量的根底上实现互斥锁只须要:

`type Mutex Semaphore`
`func NewMutex() Mutex {`
 `return Mutex(NewSemaphore(1))`
`}`

RWMutex

RWMutex是一个略微简单的原语:它容许任意数量的并发读锁,但在任何给定工夫仅容许一个写锁。还能够保障,如果有线程持有写锁,则任何线程都不能持有或取得读锁。

sync规范库里的RWMutex还容许如果有线程尝试获取写锁,则其余读锁将排队期待,以防止饿死尝试获取写锁的线程。为了简洁起见,在用channel实现的RWMutex里咱们疏忽了这部分逻辑。

RWMutex具备三种状态:闲暇,存在写锁和存在读锁。这意味着咱们须要两个通道别离标记RWMutex上的读锁和写锁:闲暇时,两个通道都为空;当获取到写锁时,标记写锁的通道里将被写入一下空构造体;当获取到读锁时,咱们向两个通道中都写入一个值(防止写锁可能向标记写锁的通道发送值),其中标记读锁的通道里的值代表以后RWMutex领有的读锁的数量,读锁开释的时候除了更新通道里存的读锁数量值,也会抽空写锁通道。

`type RWMutex struct {`
 `write   chan struct{}`
 `readers chan int`
`}`
`func NewLock() RWMutex {`
 `return RWMutex{`
 `// 用来做一个一般的互斥锁`
 `write:   make(chan struct{}, 1),`
 `// 用来爱护读锁的数量,获取读锁时通过承受通道里的值确保`
 `// 其余goroutine不会在同一时间更改读锁的数量。`
 `readers: make(chan int, 1),`
 `}`
`}`
`func (l RWMutex) Lock() { l.write <- struct{}{} }`
`func (l RWMutex) Unlock() { <-l.write }`
`func (l RWMutex) RLock() {`
 `// 统计以后读锁的数量,默认为0`
 `var rs int`
 `select {`
 `case l.write <- struct{}{}:`
 `// 如果write通道能发送胜利,证实当初没有读锁`
 `// 向write通道发送一个值,防止出现并发的读-写`
 `case rs = <-l.readers:` 
 `// 能从通道里接管到值,证实RWMutex上曾经有读锁了,上面会更新读锁数量`
 `}`
 `// 如果执行了l.write <- struct{}{}, rs的值会是0`
 `rs++`
 `// 更新RWMutex读锁数量`
 `l.readers <- rs`
`}`
`func (l RWMutex) RUnlock() {`
 `// 读出读锁数量而后减一`
 `rs := <-l.readers`
 `rs--`
 `// 如果开释后读锁的数量变为0了,抽空write通道,让write通道变为可用`
 `if rs == 0 {`
 `<-l.write`
 `return`
 `}`
 `// 如果开释后读锁的数量减一后不是0,把新的读锁数量发送给readers通道`
 `l.readers <- rs`
`}`

WaitGroup

WaitGroup最常见的用处是创立一个组,向其计数器中设置一个计数,生成与该计数一样多的goroutine,而后期待它们实现。每次goroutine运行结束后,它将在组上调用Done示意已实现工作。能够通过调用WaitGroupDone办法或以正数调用Add办法缩小计数器的计数。当计数器达到0时,被Wait办法阻塞住的主线程会复原执行。

WaitGroup一个鲜为人知的性能是在计数器达到0后,如果调用Add办法让计数器变为负数,这将使WaitGroup重回阻塞状态。这意味着对于每个给定的WaitGroup,都有一点”世代”的象征:

  • 当计数器从0移到负数时开始”世代”。
  • 当计数器重回到0时,WaitGroup的一个世代完结。
  • 当一个世代完结时,被该世代的所阻塞住的线程将复原执行。

上面是用channel实现的WaitGroup同步原语,真正起到阻塞goroutine作用的是世代里的wait通道,而后通过用WaitGroup通道包装generation构造体实现WaitGroupWaitAdd等性能。用文字很难形容分明还是间接看上面的代码吧,代码里的正文会帮忙了解实现原理。

`type generation struct {`
 `// 用于让期待者阻塞住的通道`
 `// 这个通道永远不会用于发送,只用于接管和close。`
 `wait chan struct{}`
 `// 计数器,标记须要期待执行实现的job数量`
 `n int`
`}`
`func newGeneration() generation {`
 `return generation{ wait: make(chan struct{}) }`
`}`
`func (g generation) end() {`
 `// close通道将开释因为承受通道而阻塞住的goroutine`
 `close(g.wait)`
`}`
`//这里咱们应用一个通道来爱护以后的generation。`
`//它基本上是WaitGroup状态的互斥量。`
`type WaitGroup chan generation`
`func NewWaitGroup() WaitGroup {`
 `wg := make(WaitGroup, 1)`
 `g := newGeneration()`
 `// 在一个新的WaitGroup上Wait, 因为计数器是0,会立刻返回不会阻塞住线程`
 `// 它体现跟以后世代曾经完结了一样, 所以这里先把世代里的wait通道close掉`
 `// 避免刚创立WaitGroup时调用Wait函数会阻塞线程`
 `g.end()`
 `wg <- g`
 `return wg`
`}`
`func (wg WaitGroup) Add(delta int) {`
 `// 获取以后的世代`
 `g := <-wg`
 `if g.n == 0 {`
 `// 计数器是0,创立一个新的世代`
 `g = newGeneration()`
 `}`
 `g.n += delta`
 `if g.n < 0 {`
 `// 跟sync库里的WaitGroup一样,不容许计数器为正数`
 `panic("negative WaitGroup count")`
 `}`
 `if g.n == 0 {`
 `// 计数器回到0了,敞开wait通道,被WaitGroup的Wait办法`
 `// 阻塞住的线程会被释放出来持续往下执行`
 `g.end()`
 `}`
 `// 将更新后的世代发送回WaitGroup通道`
 `wg <- g`
`}`
`func (wg WaitGroup) Done() { wg.Add(-1) }`
`func (wg WaitGroup) Wait() {`
 `// 获取以后的世代`
 `g := <-wg`
 `// 保留一个世代里wait通道的援用`
 `wait := g.wait`
 `// 将世代写回WaitGroup通道`
 `wg <- g`
 `// 接管世代里的wait通道`
 `// 因为wait通道里没有值,会把调用Wait办法的goroutine阻塞住`
 `// 直到WaitGroup的计数器回到0,wait通道被close后才会解除阻塞`
 `<-wait`
`}`

总结

明天这篇文章用通道实现了Go语言sync包里罕用的几种同步锁,次要的目标是演示通道和select语句联合后弱小的表达能力,并没有什么理论利用价值,大家也不要在理论开发中应用这里实现的同步锁。

无关通道和同步锁都适宜解决什么品种的问题咱们前面的文章再细说,明天这篇文章,须要充沛了解Go语言通道的行为能力了解文章里的代码,如果有哪里看不懂的能够留言,只有工夫容许我都会答复。

如果还不理解sync包里的同步锁的应用办法,请先看这篇文章 Go语言sync包的利用详解。下一篇文章我会介绍并发编程里的数据竞争问题以及解决办法,以及思考给大家留一道思考题。

参考链接:https://blogtitle.github.io/g…

举荐浏览

  • 并发平安的 map:sync.Map源码剖析

喜爱本文的敌人,欢送关注“Go语言中文网”:

Go语言中文网启用微信学习交换群,欢送加微信:274768166,投稿亦欢送

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理