深入理解Go语言并发控制

文章介绍了Go语言中处理数据争用的方法,包括使用context进行协程的优雅关闭,以及sync/atomic包中的CompareAndSwap实现自旋锁。接着详细讨论了互斥锁的实现,包括快速路径、慢路径、信号量和等待队列。此外,还提到了读写锁在并发控制中的作用。整体来看,Go语言的锁机制结合了多种技术以提高性能。
摘要由CSDN通过智能技术生成

数据争用是并发系统中最常见且最难调试的错误类型之一。

目录

context

数据争用检查

sync/atomic中CompareAndSwap

互斥锁

读写锁

总结


context

context用于优雅地关闭具有存在级联关系的协程。若没有context,管理协程退出需要借助通道close机制,该机制会唤醒所有监听这个通道的协程,并触发退出逻辑,而context可以说利用了通道close机制管理协程间的关闭机制。

type Context interface{
    Deadline() (deadline time.Time,ok bool)
    Done() <-chan struct{}
    Err() error
}

Deadline返回两个返回值,一个是还有多久到期,一个是是否到期。

Done是最常见的方法,返回一个通道,一般监听这个通道的协程若收到信息则表示协程已关闭,需要执行退出。

Err 返回退出原因。

func main()  {
    ctx,cancel := context.WithCancel(context.Background())
    go Speak(ctx)
    time.Sleep(10*time.Second)
    cancel()
    time.Sleep(1*time.Second)
}

func Speak(ctx context.Context)  {
    for range time.Tick(time.Second){
        select {
        case <- ctx.Done():
            fmt.Println("我要闭嘴了")
            return
        default:
            fmt.Println("balabalabalabala")
        }
    }
}

当main运行到cancel()后,表示关闭这个ctx,则Speak子协程就会监听到ctx.Done()从而关闭。

运行结果:

balabalabalabala
....省略
balabalabalabala
我要闭嘴了

Context是一个接口,我可以自定义实现这个接口,而一般的是调用GO标准库的简单实现。调用context.Background或者context.TODO函数,这个两个一般最为根对象创建出根context。一般地,我们会利用根对象派生出子对象,而派生的子对象又有各自的特点可以使用。具体函数:

func WithCancel(parent Context) (ctx Context,cancel CancelFunc)
func WithTimeout ...
func WithDeadline ..
func withValue ..

所以,context的退出传播关系是父context的退出会导致所有子context退出,而子context的退出不会影响父context的退出。

数据争用检查

数据争用Go语言中可以使用data race工具检测,data race使用底层使用矢量时钟技术用来观察时间之间happened_before的顺序。

GO语言内存模型中,只保证一个协程的执行顺序,即时是编译器重排也只能保证在单单这个协程执行结果和原始代码一致,而不能保证其他协程观察到的写入顺序。其次,程序中执行顺序不同的内存访问也会存在不安全的问题。例如:多处理器包含多存储缓冲区的情景下。

sync/atomic中CompareAndSwap

通过sync/atomic包中原子操作,我们能构建一种自旋锁,直到获取该锁,才能执行区域中的代码。

互斥锁

通过原子操作构建的互斥锁--自旋锁虽然好用,但是当一个协程长时间占有锁的话其他协程将毫无意义地消耗CPU资源。所以这里还有一种互斥锁,其实现方式即包含自旋锁,又参考了操作系统锁的原理。Sync.Mutex

type Mutes struct {
    state int32  //锁的状态
    sema uint32  //锁的信号量
}

state通过位图表示当前锁的状态

 若协程长时间无法获取锁则会进入饥饿模式。

获取锁操作:

func (m *Mutex) Lock(){
    //快速路径
    if atomic.CompareAndSwapInt32(&m.state,0,mutexLocked){
        return 
    }
    //慢路径
    m.lockSlow()
}

一开始,走快速路径,若获取到锁则直接往下走,若没有获取,则走进慢路径实际上就是自旋状态。而若有以下4种情况之一,则自旋停止:

1,程序在单核CPU上执行

2,逻辑处理器P小于等于1

3,当前协程所在的逻辑处理器P在本地队列上有其他协程等待运行。

4,自旋次数超过一定的阈值。

若慢路径还没获取锁,则修改信号量。即信号量-1,协程休眠;


实际上,互斥锁的信息会根据锁的地址存储在全局哈希表semtable哈希表中。这个哈希表用来存储所有的互斥锁以及等待该锁的协程

其中,哈希表中的链表还勾着成一种引入随机树的二叉搜索树。引入随机树及必要时的旋转保证了比较好的平衡性。设计为二叉搜索树是为了能够快速找到保存过的锁,以log2N的速度查找,若已存在该锁则将等待待协程放入队列尾。放入队列中的协程若长时间得不到锁,会进入饥饿状态,,这时新申请的协程不会自旋,而是直接放入等待队列中。放入队列后,协程会切换状态让渡执行权重新调度,这样GMP模型下线程就不会停止工作。

当然,访问这个哈希表时也会面临数据并发,所以这里也需要加锁,其方式是若自旋一定次数得不到锁则调用操作系统的互斥锁(linux: pthread mutex) 

释放锁

释放锁同样有快速路径和慢路径。

func (m *mutex) unlock(){
    //快速路径
    new := atomic.AddInt32(&m.state,-mutexLocked)
    //慢路径
     if new !=0 {
        m.unlockSlow(new)
    }
}

若无等待协程则直接走快速路径释放锁结束。

有的话,则信号量同步,到全局哈希表找当前锁等待协程,唤醒该协程后,将协程放到逻辑处理器P的runnext字段,表示下一次就调度这个协程即优先调度。

读写锁

总结

GO语言中的互斥锁算一种混合锁,结合原子操作,自旋,信号量,全局哈希表,等待队列,操作系统级别锁等等。但是GO语言的锁相对于操作系统的锁更快,因为绝大部分情况停留在用户态。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值