网站首页 > 文章中心 > 其它

go语言sync互锁

作者:小编 更新时间:2023-08-18 13:34:45 浏览量:123人看过

Go sync/atomic包Load和Store并发不安全

前言:为了保证并发安全,go语言中可以使用原子操作.其执行过程不能被中断,这也就保证了同一时刻一个线程的执行不会被其他线程中断,也保证了多线程下数据操作的一致性.

对于每一种类型,提供了五类原子操作分别是

Load和Store操作对应与变量的原子性读写,许多变量的读写无法在一个时钟周期内完成,而此时执行可能会被调度到其他线程,无法保证并发安全.

Load 只保证读取的不是正在写入的值,Store只保证写入是原子操作.

go语言sync互锁-图1

所以在使用的时候要注意.

下面简单示例:

golang sync.pool对象复用 并发原理 缓存池

在go http每一次go serve(l)都会构建Request数据结构.在大量数据请求或高并发的场景中,频繁创建销毁对象,会导致GC压力.解决办法之一就是使用对象复用技术.在http协议层之下,使用对象复用技术创建Request数据结构.在http协议层之上,可以使用对象复用技术创建(w,*r,ctx)数据结构.这样即可以回快TCP层读包之后的解析速度,也可也加快请求处理的速度.

先上一个测试:

结论是这样的:

go语言sync互锁-图2

貌似使用池化,性能弱爆了?这似乎与net/http使用sync.pool池化Request来优化性能的选择相违背.这同时也说明了一个问题,好的东西,如果滥用反而造成了性能成倍的下降.在看过pool原理之后,结合实例,将给出正确的使用方法,并给出预期的效果.

sync.Pool是一个 协程安全 的 临时对象池 .数据结构如下:

local 成员的真实类型是一个 poolLocal 数组,localSize 是数组长度.这涉及到Pool实现,pool为每个P分配了一个对象,P数量设置为runtime.GOMAXPROCS(0).在并发读写时,goroutine绑定的P有对象,先用自己的,没有去偷其它P的.go语言将数据分散在了各个真正运行的P中,降低了锁竞争,提高了并发能力.

不要习惯性地误认为New是一个关键字,这里的New是Pool的一个字段,也是一个闭包名称.其API:

如果不指定New字段,对象池为空时会返回nil,而不是一个新构建的对象.Get()到的对象是随机的.

原生sync.Pool的问题是,Pool中的对象会被GC清理掉,这使得sync.Pool只适合做简单地对象池,不适合作连接池.

pool创建时不能指定大小,没有数量限制.pool中对象会被GC清掉,只存在于两次GC之间.实现是pool的init方法注册了一个poolCleanup()函数,这个方法在GC之前执行,清空pool中的所有缓存对象.

注意到pool中的对象是无差异性的,加锁或者分段加锁都不是较好的做法.go的做法是为每一个绑定协程的P都分配一个子池.每个子池又分为私有池和共享列表.共享列表是分别存放在各个P之上的共享区域,而不是各个P共享的一块内存.协程拿自己P里的子池对象不需要加锁,拿共享列表中的就需要加锁了.

Get对象过程:

Put过程:

如何解决Get最坏情况遍历所有P才获取得对象呢:

执行结果:

单线程情况下,遍历其它无元素的P,长时间加锁性能低下.启用协程改善.

结果:

测试场景在goroutines远大于GOMAXPROCS情况下,与非池化性能差异巨大.

测试结果

可以看到同样使用*sync.pool,较大池大小的命中率较高,性能远高于空池.

池pool和缓存cache的区别.池的意思是,池内对象是可以互换的,不关心具体值,甚至不需要区分是新建的还是从池中拿出的.缓存指的是KV映射,缓存里的值互不相同,清除机制更为复杂.缓存清除算法如LRU、LIRS缓存算法.

池空间回收的几种方式.一些是GC前回收,一些是基于时钟或弱引用回收.最终确定在GC时回收Pool内对象,即不回避GC.用java的GC解释弱引用.GC的四种引用:强引用、弱引用、软引用、虚引用.虚引用即没有引用,弱引用GC但有空间则保留,软引用GC即清除.ThreadLocal的值为弱引用的例子.

regexp 包为了保证并发时使用同一个正则,而维护了一组状态机.

fmt包做字串拼接,从sync.pool拿[]byte对象.避免频繁构建再GC效率高很多.

Go语言——sync.Map详解

go1.10\src\sync\map.go

entry分为三种情况:

从read中读取key,如果key存在就tryStore.

注意这里开始需要加锁,因为需要操作dirty.

条目在read中,首先取消标记,然后将条目保存到dirty里.(因为标记的数据不在dirty里)

最后原子保存value到条目里面,这里注意read和dirty都有条目.

最后提醒一下大家Store:

这里可以看到dirty保存了数据的修改,除非可以直接原子更新read,继续保持read clean.

有了之前的经验,可以猜测下load流程:

与猜测的 区别 :

由于数据保存两份,所以删除考虑:

先看第二种情况.加锁直接删除dirty数据.思考下貌似没什么问题,本身就是脏数据.

第一种和第三种情况唯一的区别就是条目是否被标记.标记代表删除,所以直接返回.否则CAS操作置为nil.这里总感觉少点什么,因为条目其实还是存在的,虽然指针nil.

看了一圈貌似没找到标记的逻辑,因为删除只是将他变成nil.

之前以为这个逻辑就是简单的将为标记的条目拷贝给dirty,现在看来大有文章.

p == nil,说明条目已经被delete了,CAS将他置为标记删除.然后这个条目就不会保存在dirty里面.

这里其实就跟miss逻辑串起来了,因为miss达到阈值之后,dirty会全量变成read,也就是说标记删除在这一步最终删除.这个还是很巧妙的.

真正的删除逻辑:

很绕....

为什么我不喜欢Go语言式的接口

所谓Go语言式的接口,就是不用显示声明类型T实现了接口I,只要类型T的公开方法完全满足接口I的要求,就可以把类型T的对象用在需要接口I的地方.这种做法的学名叫做Structural Typing,有人也把它看作是一种静态的Duck Typing.除了Go的接口以外,类似的东西也有比如Scala里的Traits等等.有人觉得这个特性很好,但我个人并不喜欢这种做法,所以今天这一节谈谈它的缺点.当然这跟动态语言静态语言的讨论类似,不能简单粗暴的下一个"好"或"不好"的结论.

我的观点:

Go的隐式接口Duck Typing确实不是新技术, 但是在主流静态编程语言中支持Duck Typing应该是很少的(不清楚目前是否只有Go语言支持).

静态类型和动态类型虽然没有绝对的好和不好, 但是每个都是有自己的优势的, 没有哪一个可以包办一切. 而Go是试图结合静态类型和动态类型(interface)各自的优势.

那么就从头谈起:什么是接口.其实通俗的讲,接口就是一个协议,规定了一组成员,例如.NET里的ICollection接口:

go语言sync互锁-图3

public interface ICollection {

int Count { get; }

object SyncRoot { get; }

bool IsSynchronized { get; }

void CopyTo(Array array, int index);

}

这就是一个协议的全部了吗?事实并非如此,其实接口还规定了每个行为的"特征".打个比方,这个接口的Count除了需要返回集合内元素的数目以外,还隐含了它需要在O(1)时间内返回这个要求.这样一个使用了ICollection接口的方法才能放心地使用Count属性来获取集合大小,才能在知道这些特征的情况下选用正确的算法来编写程序,而不用担心带来性能问题,这才能实现所谓的"面向接口编程".当然这种"特征"并不但指"性能"上的,例如Count还包含了例如"不修改集合内容"这种看似十分自然的隐藏要求,这都是ICollection协议的一部分.

Go语言设计与实现(上)

基本设计思路:

类型转换、类型断言、动态派发.iface,eface.

反射对象具有的方法:

编译优化:

内部实现:

实现 Context 接口有以下几个类型(空实现就忽略了):

互斥锁的控制逻辑:

设计思路:

(以上为写被读阻塞,下面是读被写阻塞)

总结,读写锁的设计还是非常巧妙的:

WaitGroup 有三个暴露的函数:

部件:

结构:

Once 只暴露了一个方法:

实现:

三个关键点:

细节:

让多协程任务的开始执行时间可控(按顺序或归一).(Context 是控制结束时间)

设计思路: 通过一个锁和内置的 notifyList 队列实现,Wait() 会生成票据,并将等待协程信息加入链表中,等待控制协程中发送信号通知一个(Signal())或所有(Boardcast())等待者(内部实现是通过票据通知的)来控制协程解除阻塞.

暴露四个函数:

实现细节:

包: golang.org/x/sync/errgroup

作用:开启 func() error 函数签名的协程,在同 Group 下协程并发执行过程并收集首次 err 错误.通过 Context 的传入,还可以控制在首次 err 出现时就终止组内各协程.

暴露的方法:

注意问题:

包: "golang.org/x/sync/semaphore"

作用:排队借资源(如钱,有借有还)的一种场景.此包相当于对底层信号量的一种暴露.

设计思路:有一定数量的资源 Weight,每一个 waiter 携带一个 channel 和要借的数量 n.通过队列排队执行借贷.

暴露方法:

包: "golang.org/x/sync/singleflight"

作用:防击穿.瞬时的相同请求只调用一次,response 被所有相同请求共享.

设计思路:按请求的 key 分组(一个 *call 是一个组,用 map 映射存储组),每个组只进行一次访问,组内每个协程会获得对应结果的一个拷贝.

逻辑:

如有错误,请批评指正.

版权声明:倡导尊重与保护知识产权。未经许可,任何人不得复制、转载、或以其他方式使用本站《原创》内容,违者将追究其法律责任。本站文章内容,部分图片来源于网络,如有侵权,请联系我们修改或者删除处理。

编辑推荐

热门文章