Mutex
count++操作问题
count++是非原子操作,所以有并发问题
什么是原子操作
所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch(操作系统保证)。所以原子操作在单处理器中是没有并发问题的。
但是光靠系统,无法保证多处理器下原子操作的并发安全
例子:
以decl (递减指令)为例,这是一个典型的"读-改-写"过程,涉及两次内存访问。设想在不同CPU运行的两个进程都在递减某个计数值,可能发生的情况是:
⒈ CPU A(CPU A上所运行的进程,以下同)从内存单元把当前计数值⑵装载进它的寄存器中;
⒉ CPU B从内存单元把当前计数值⑵装载进它的寄存器中。
⒊ CPU A在它的寄存器中将计数值递减为1;
⒋ CPU B在它的寄存器中将计数值递减为1;
⒌ CPU A把修改后的计数值⑴写回内存单元。
⒍ CPU B把修改后的计数值⑴写回内存单元。
所以,多处理器下原子操作的并发安全需要硬件的支持,和CPU的架构有关:
在x86 平台上,CPU提供了在指令执行期间对总线加锁的手段。如果汇编语言的程序中在一条指令前面加上前缀"LOCK",经过汇编以后的机器代码就使CPU在执行这条指令的时候把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的并发安全。
可以加LOCK前缀的指令:
只有ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG(CAS操作),DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD 和 XCHG指令前面可以加"LOCK"指令,实现原子操作
atomic解决count++问题
atomic是GO提供的原子操作包,这里的count++就可以使用atomic.Add(),这样就可以保证count值增加1的并发安全
CAS的ABA问题
假设有两个线程T1和T2,这两个线程对同一个栈进行出栈和入栈的操作
出栈操作:CAS(SP, newSP)
入栈操作:CAS(SP, newEle)
SP代表栈顶,newSP代表栈顶下一个元素
T1、T2操作前,栈为A->B->C
T1先准备出栈,拿到SP是A,newSP是B,执行CAS(A, B),执行CAS前B先执行了出栈、出栈、再把A入栈,栈变为A->C
这时T1执行CAS(A, B),因为当前的SP还是A,所以栈顶就变为了B,但是和预期不符,现在链表上只有A、C,T1出栈后栈顶应该是C
解决ABA问题的手段
加一个版本号,每次修改值的时候先使用CAS操作修改版本号,只有版本号修改成功才可以使用CAS操作修改值
假设初始版本号是1
这样之前的出栈、入栈问题,T1的操作就是CAS(expected oldValue A, newValue B, expected oldVersion 1, newVersion 2),T2三次栈操作后version变为4,T1的CAS操作就会失败
References
https://blog.csdn.net/qq_35492857/article/details/78471032
https://blog.csdn.net/weixin_42914675/article/details/103983870
Mutex演进
初版Mutex
使用一个flag变量表示申请锁的goroutine数量
加锁时使用CAS操作设置flag变量的值为当前值+1,如果+1后的值是1那么证明拿到锁,否则阻塞等待
解锁时使用CAS操作设置flag变量的值为当前值-1, 如果-1后的值是0那么证明没有等待者,否则说明有等待者那么唤醒等待者
由于每次加锁是将锁变量的值+1,所以如果有等待者那么新来的goroutine就拿不到锁
type Mutex struct {
key int32
sema int32 //信号量,用来阻塞、唤醒goroutine
}
func (m *Mutex)Lock() {
for {
if xadd(&m.key, 1) == 1 {
return
}
runtime_semacquire(&m.sema)
}
}
fun xadd(value *int, delta int) int {
for {
if cas(value, *value, *value+delta) {
return *value+delta
}
}
}
func (m *Mutex)UnLock() {
if xadd(&m.key, -