乐趣区

关于后端:面试官让我用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,投稿亦欢送

退出移动版