go func() {
for i := 0; i < 20; i++ {
time.Sleep(time.Millisecond)
atomic.AddInt32(&cnt, 1)
}
}()
}
time.Sleep(time.Second)
cntFinal := atomic.LoadInt32(&cnt)
fmt.Println("cnt: ", cntFinal)
}
神奇CAS的原理
Golang的AddInt32()类似于Java中AtomicInteger.incrementAndGet(),其伪代码可以表示如下。二者的基本思想是一致的,本质上是 乐观锁:首先,从内存位置M加载要修改的数据到寄存器A中;然后,修改数据并保存到另一寄存器B;最终,利用CPU提供的CAS指令(Java通过JNI调用到)用一条指令完成:1)A值与M处的原值比较;2)若相同则将B值覆盖到M处。
若不相同,则CAS指令会失败,说明从内存加载到执行CAS指令这一小段时间内,发生了上下文切换,执行了其他线程的代码修改了M处的变量值。那么重新执行前面几个步骤再次尝试。
ABA问题:即另一线程修改了M位置的数据,但是从原值改为C,又从C改回原值。这样上下文切换回来,CAS指令发现M处的值“未改变”(实际是改了两次,最后改回来了),所以CAS指令正常执行,不会失败。这种问题在Java中可以用AtomicStampedReference/AtomicMarkableReference解决。
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
4.3 Channel管道
通过前面可以看到,尽管goroutine很方便很高效,但如果滥用的话很可能会导致并发安全问题。而Channel就是用来解决这个问题的,它是goroutine之间通信的桥梁,类似Actor模型中每个Actor的mailbox。多个goroutine要修改一个状态时,可以将请求都发送到一个Channel里,然后由一个goroutine负责顺序地修改状态。
Channel默认是阻塞的,也就是说select时如果没有事件,那么当前goroutine会发生读阻塞。同理,Channel是有大小的,当Channel满了时,发送方会发生写阻塞。Channel这种阻塞的特性加上goroutine可以很容易就能实现生产者-消费者模式。
用case可以给Channel设置阻塞的超时时间,避免一直阻塞。而default则使select进入无阻塞模式。









