go的锁: https://www.infoq.com/presentations/go-locks/
- 直接用一个flag为啥不行?
- 不是原子性的 not atomic
- reordering 指令重排序
- hardware可以实现
- x86: XCHG
- LOCK prefix 很好使(一切的基础)
- LOCK ADD
- LOCK CMPXCHG
- 一步一步地实现,改进
- 最基础的实现,利用上面的指令CAS,spinslock,循环检测。问题:死循环检测导致CPU浪费
- 改进:futex
- flag int: 0: unlocked; 1:locked; 2: there's a waiter
- 逻辑:自旋失败的时候,线程可以去睡觉,等到flag修改的时候再醒来
- 读方法:
- 死循环
- if (atomic.cas(&flag, 0, 1))
- 获得锁 ,干活
- CAS失败了, 设置flag为sleeping
- atomic.xchg(&flag, 2)
- futex(&flag, FUTEX_WAIT, 2) 这样kernal就是put当前线程sleep,当别的线程调用FUTEX_WAKE的时候叫醒我。
- if (atomic.cas(&flag, 0, 1))
- 死循环
- 写方法:
- 死循环
- if(atomic.cas(&flag,0,1))
- 干活
- 将flag设置为unlocked: v := atomic.Xchg(&flag, 0)
- 如果有人在等,那就叫醒他:
- if v == 2
- futex(&flag, FUTEX_WAKE, ...)
- if(atomic.cas(&flag,0,1))
- 死循环
- 读方法:
- interface:futex syscall
- mechanism:kernal-managed queue
- kernal怎么实现futex的?
- 哈希表,链表法解决冲突。利用用户空间的内存地址做哈希,找到一个bucket,放置我们要suspend的线程
- 挂起线程
- cost
- 无竞争(单线程)~13ns,cost of user-space atomic CAS = ~13ns
- 有竞争(12cpu)0.9us,900ns,cost of user-space atomic CAS + syscall + thread context switch = ~0.9us
- 所以是sleep好还是spin好?
- 如果获取锁的时间短,那就spin好了
- 如果长,那就挂起
- 那就混合起来吧:hybrid
- 先自旋几次,自旋几次获取不到了,那就调用futex syscall。
- 这个操作在go里面有,在java的锁升级过程也是这样。
- go在user-space搞的,能不能做得更好?
- 由于整个go都是运行在线程里的,切换goroutine只需10ns,线程切换需要a us
- we can block the goroutine without bloking the underlying thread
- to avoid the thread context switch cost
- go来实现goroutine切换,调度(scheduling)
- we can block the goroutine without bloking the underlying thread
- 这就是 go semaphore
- 与futex很类似,不过用的是sleep/wake goroutine
- 由于整个go都是运行在线程里的,切换goroutine只需10ns,线程切换需要a us
- 还有啥问题
- 被唤醒的goroutine很可能被在CAS竞争中输掉:there's a delay between when the flag was set to 0 and this goroutine was rescheduled
- 解决:sync.Mutex
- 实际上在等待队列中,还是需要lock:each hash bucket needs a lock:it's a futex
- sync.Mutex
- uses a semaphore
- uses futexes(per hash bucket)锁住每个hash表里的bucket来实现高并发的同步问题
- uses spin-locks(futex也是用的哈希表,他也要处理哈希表冲突问题)
- uses futexes(per hash bucket)锁住每个hash表里的bucket来实现高并发的同步问题
- uses a semaphore