go 互斥锁和自旋锁的性能对比的问题

Mericusta · 2022-08-18 11:59:34 · 2851 次点击 · 大约8小时之前 开始浏览    置顶
这是一个创建于 2022-08-18 11:59:34 的主题,其中的信息可能已经有所发展或是发生改变。

最近需要做一个本地缓存的需求,缓存数据从 redis 读取 遂考虑到在大量并发的情况下,减少 go 协程切换的消耗,考虑用自旋锁来实现

大概的思路是:

  • 大量并发请求
  • 自旋锁上锁
    • 放一个 goroutine 去 redis 读取数据到本地缓存
    • 其他 goroutine 原地自旋等待缓存数据
  • 读取的协程读取到数据之后解锁

但是我经过实际测试,发现用自旋锁的速度和用互斥锁的速度要差1~2个数量级,遂很不理解

之后我经过大量测试发现:

  • 自旋锁在 本地操作(值自增),阻塞操作(sleep 或者 channel recv)时,性能高于互斥锁
  • 在 HTTP 操作时性能和互斥锁相当
  • 在通过 "github.com/go-redis/redis/v8" 操作 redis 获取数据时,自旋锁的性能和互斥锁要差1个数量级

不知道是否有大佬可以解答一番?不吝赐教

测试代码在这里


有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2851 次点击  ∙  1 赞  
加入收藏 微博
15 回复  |  直到 2022-09-21 10:21:21
liangmanlin
liangmanlin · #1 · 3年之前

为什么要自旋,一个channel就可以解决

fginter
fginter · #2 · 3年之前

是的,多协程数据传递,Go已经给了你语言级别解决方案--channel

Mericusta
Mericusta · #3 · 3年之前
liangmanlinliangmanlin #1 回复

为什么要自旋,一个channel就可以解决

channel 不适用于此场景

Mericusta
Mericusta · #4 · 3年之前
fginterfginter #2 回复

是的,多协程数据传递,Go已经给了你语言级别解决方案--channel

channel 不适用于此场景

etog
etog · #5 · 3年之前

能不能别上来就撸锁。

你这个场景可以调整成用协程池来控制并发量。

liangmanlin
liangmanlin · #6 · 3年之前
MericustaMericusta #3 回复

#1楼 @liangmanlin channel 不适用于此场景

怎么就不适用?channel也是抢占的调度,从功能上是一样的,但是channel会让出cpu,这是高并发的首选

Mericusta
Mericusta · #7 · 3年之前
etogetog #5 回复

能不能别上来就撸锁。 你这个场景可以调整成用协程池来控制并发量。

直接用了 HTTP 框架 gin,gin 的请求就是并发处理的,相当于 handler 已经运行在了某一个协程上,在此场景下,先去本地内存缓存获取数据,没有则去 redis 获取数据,为了减少 redis 的访问量,这里只放了一个 协程 去 redis 获取数据,其他协程等待

并发量大的情况下协程阻塞导致协程切换,可能会带来较多的性能损耗,从而延长某些请求的响应时间,所以想采用自旋锁来做不阻塞的等待

channel 不适用于此场景

Mericusta
Mericusta · #8 · 3年之前
liangmanlinliangmanlin #6 回复

#3楼 @Mericusta 怎么就不适用?channel也是抢占的调度,从功能上是一样的,但是channel会让出cpu,这是高并发的首选

见5楼

liangmanlin
liangmanlin · #9 · 3年之前
MericustaMericusta #8 回复

#6楼 @liangmanlin 见5楼

这个做法不敢苟同,如果读取redis的时间很长(事实上也是很慢,单次往返肯定在毫秒级别),并发量大的时候cpu空转的时间非常长。正确的做法绝对是先使用原子操作,判断当前是否有缓存,如果没有,使用channel等待,自旋锁100%会降低吞吐量

Mericusta
Mericusta · #10 · 3年之前
liangmanlinliangmanlin #9 回复

#8楼 @Mericusta 这个做法不敢苟同,如果读取redis的时间很长(事实上也是很慢,单次往返肯定在毫秒级别),并发量大的时候cpu空转的时间非常长。正确的做法绝对是先使用原子操作,判断当前是否有缓存,如果没有,使用channel等待,自旋锁100%会降低吞吐量

redis 等涉及操作系统的网络操作,经过我的测试,确实使用自旋锁效率要差一些,所以我最终选用了互斥锁的做法

这里不用 channel 是因为没有协程之间交互的需求,这只是一个并发读写某块内存的需求

GO_go_GO1
GO_go_GO1 · #11 · 3年之前
liangmanlinliangmanlin #1 回复

为什么要自旋,一个channel就可以解决

channel也是锁

GO_go_GO1
GO_go_GO1 · #12 · 3年之前
MericustaMericusta #7 回复

#5楼 @etog 直接用了 HTTP 框架 gin,gin 的请求就是并发处理的,相当于 handler 已经运行在了某一个协程上,在此场景下,先去本地内存缓存获取数据,没有则去 redis 获取数据,为了减少 redis 的访问量,这里只放了一个 协程 去 redis 获取数据,其他协程等待 并发量大的情况下协程阻塞导致协程切换,可能会带来较多的性能损耗,从而延长某些请求的响应时间,所以想采用自旋锁来做不阻塞的等待 channel 不适用于此场景

说白了就是 多个连接并发访问全局变量,这个变量没有就读redis ,有就直接读内存,全局变量要用互斥锁,如果锁是你们服务的性能瓶颈而非网络io,那就考虑分片锁

Mericusta
Mericusta · #13 · 3年之前
GO_go_GO1GO_go_GO1 #12 回复

#7楼 @Mericusta 说白了就是 多个连接并发访问全局变量,这个变量没有就读redis ,有就直接读内存,全局变量要用互斥锁,如果锁是你们服务的性能瓶颈而非网络io,那就考虑分片锁

不管是从理论上还是实际测试的情况来看,网络 IO 肯定是性能瓶颈,操作系统的套接字阻塞住会导致运行 G 的 M 阻塞住,而互斥锁只是导致 G 阻塞住,G 的上下文切换效率肯定是优于 M 的上下文切换的

之前考虑用自旋锁就是想能不能在这个基础上再做 G 上下文切换的优化,毕竟请求的量不少,也就是 G 不少的,至于分片锁,应该应用不到这个场景里,数据没有分片的需求

GO_go_GO1
GO_go_GO1 · #14 · 3年之前
MericustaMericusta #13 回复

#12楼 @GO_go_GO1 不管是从理论上还是实际测试的情况来看,网络 IO 肯定是性能瓶颈,操作系统的套接字阻塞住会导致运行 G 的 M 阻塞住,而互斥锁只是导致 G 阻塞住,G 的上下文切换效率肯定是优于 M 的上下文切换的 之前考虑用自旋锁就是想能不能在这个基础上再做 G 上下文切换的优化,毕竟请求的量不少,也就是 G 不少的,至于分片锁,应该应用不到这个场景里,数据没有分片的需求

goroutine 的切换 是哪个层面的切换,这个有必要考虑进性能问题吗?

Mericusta
Mericusta · #15 · 3年之前
GO_go_GO1GO_go_GO1 #14 回复

#13楼 @Mericusta goroutine 的切换 是哪个层面的切换,这个有必要考虑进性能问题吗?

协程的上下文切换,是在性能优化的考虑范畴之内,协程虽然比线程轻量,但也有性能上限,不能随意开

添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传