Go并发编程之美-CAS操作

Python011

Go并发编程之美-CAS操作,第1张

摘要: 一、前言 go语言类似Java JUC包也提供了一些列用于多线程之间进行同步的措施,比如低级的同步措施有 锁、CAS、原子变量操作类。相比Java来说go提供了独特的基于通道的同步措施。本节我们先来看看go中CAS操作 二、CAS操作 go中的Cas操作与java中类似,都是借用了CPU提供的原子性指令来实现。

go语言类似Java JUC包也提供了一些列用于多线程之间进行同步的措施,比如低级的同步措施有 锁、CAS、原子变量操作类。相比Java来说go提供了独特的基于通道的同步措施。本节我们先来看看go中CAS操作

go中的Cas操作与java中类似,都是借用了CPU提供的原子性指令来实现。CAS操作修改共享变量时候不需要对共享变量加锁,而是通过类似乐观锁的方式进行检查,本质还是不断的占用CPU 资源换取加锁带来的开销(比如上下文切换开销)。下面一个例子使用CAS来实现计数器

go中CAS操作具有原子性,在解决多线程操作共享变量安全上可以有效的减少使用锁所带来的开销,但是这是使用cpu资源做交换的。

我简单列举了并发编程的大纲,需要详细的私信“555”~~

atomic包是go中在并发情况下必用到的包,可以基于原子性对数值进行操作,所以经常用来加减锁操作,这个包的注释里面解释了这个包的作用,还有那句著名的 用通信的方式来共享内存,不要用共享内存方式来通信。

// Share memory by communicating

// don't communicate by sharing memory.

sync/atomic中的几个函数可以对几种简单的类型进行原子操作。这些类型包括int32,int64,uint32,uint64,uintptr,unsafe.Pointer,共6个。这些函数的原子操作共有5种:增或减,比较并交换、载入、存储和交换它们提供了不同的功能,切使用的场景也有区别。

常用的方法有几种

1、增或减

给值增加或减少一个值。

第一个参数是原值,第二个是要增加多少(如果是负数就是减少多少)

2、比较并交换

比较并交换----Compare And Swap 简称CAS 这也是sync进行锁操作的基础,sync mutex 就是基于这个此方法来判断是否获取了锁

第一个参数是要判断的值,第二个参数是旧值,第三个是新值,第一个和第二个值进行判断,如果一样,就用第三个参数新值来进行替换。根据这个判断可以判断锁是否已被占用。

3、读取

原子性的读取一个值

4、写入

与读操作对应的是写入操作,sync/atomic也提供了与原子的值载入函数相对应的原子的值存储函数。这些函数的名称均以“Store”为前缀

5、交换

与CAS不同,交换操作直接赋予新值,不管旧值。返回值是旧值

总结:atomic包适用于并发量大的情况下对值的修改情况,因为是原子操作,可以做计数器或者加锁等功能。下一篇准备对go string包进行一下分析

Go 的select语句是一种仅能用于channl发送和接收消息的专用语句,此语句运行期间是阻塞的;当select中没有case语句的时候,会阻塞当前的groutine。所以,有人也会说select是用来阻塞监听goroutine的。

还有人说:select是Golang在语言层面提供的I/O多路复用的机制,其专门用来检测多个channel是否准备完毕:可读或可写。

以上说法都正确。

我们来回顾一下是什么是 I/O多路复用 。

每来一个进程,都会建立连接,然后阻塞,直到接收到数据返回响应。

普通这种方式的缺点其实很明显:系统需要创建和维护额外的线程或进程。因为大多数时候,大部分阻塞的线程或进程是处于等待状态,只有少部分会接收并处理响应,而其余的都在等待。系统为此还需要多做很多额外的线程或者进程的管理工作。

为了解决图中这些多余的线程或者进程,于是有了"I/O多路复用"

每个线程或者进程都先到图中”装置“中注册,然后阻塞,然后只有一个线程在”运输“,当注册的线程或者进程准备好数据后,”装置“会根据注册的信息得到相应的数据。从始至终kernel只会使用图中这个黄黄的线程,无需再对额外的线程或者进程进行管理,提升了效率。

select的实现经历了多个版本的修改,当前版本为:1.11

select这个语句底层实现实际上主要由两部分组成: case语句 和 执行函数 。

源码地址为:/go/src/runtime/select.go

每个case语句,单独抽象出以下结构体:

结构体可以用下图表示:

然后执行select语句实际上就是调用 func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) 函数。

func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) 函数参数:

selectgo 返回所选scase的索引(该索引与其各自的select {recv,send,default}调用的序号位置相匹配)。此外,如果选择的scase是接收操作(recv),则返回是否接收到值。

谁负责调用 func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) 函数呢?

在 /reflect/value.go 中有个 func rselect([]runtimeSelect) (chosen int, recvOK bool) 函数,此函数的实现在 /runtime/select.go 文件中的 func reflect_rselect(cases []runtimeSelect) (int, bool) 函数中:

那谁调用的 func rselect([]runtimeSelect) (chosen int, recvOK bool) 呢?

在 /refect/value.go 中,有一个 func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool) 的函数,其调用了 rselect 函数,并将最终Go中select语句的返回值的返回。

以上这三个函数的调用栈按顺序如下:

这仨函数中无论是返回值还是参数都大同小异,可以简单粗暴的认为:函数参数传入的是case语句,返回值返回被选中的case语句。

那谁调用了 func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool) 呢?

可以简单的认为是系统了。

来个简单的图:

前两个函数 Select 和 rselect 都是做了简单的初始化参数,调用下一个函数的操作。select真正的核心功能,是在最后一个函数 func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) 中实现的。

打乱传入的case结构体顺序

锁住其中的所有的channel

遍历所有的channel,查看其是否可读或者可写

如果其中的channel可读或者可写,则解锁所有channel,并返回对应的channel数据

假如没有channel可读或者可写,但是有default语句,则同上:返回default语句对应的scase并解锁所有的channel。

假如既没有channel可读或者可写,也没有default语句,则将当前运行的groutine阻塞,并加入到当前所有channel的等待队列中去。

然后解锁所有channel,等待被唤醒。

此时如果有个channel可读或者可写ready了,则唤醒,并再次加锁所有channel,

遍历所有channel找到那个对应的channel和G,唤醒G,并将没有成功的G从所有channel的等待队列中移除。

如果对应的scase值不为空,则返回需要的值,并解锁所有channel

如果对应的scase为空,则循环此过程。

在想想select和channel做了什么事儿,我觉得和多路复用是一回事儿