【go基础】11.atomic与锁 Mutex, RWMutex

目录

如何保证线程安全

几种锁

1、常用的锁

2、乐观锁与悲观锁

3、死锁

Mutex互斥锁

1、Mutex结构

2、自旋的goroutine

3、锁的两种模式

RWMutex读写锁

1、读写限制

2、写锁

3、读锁

atomic原语

1、原子操作与互斥锁区别

2、方法

3、atomic.Value


如何保证线程安全

1、同步机制:

确保多个线程在访问共享数据时不会发生冲突

2、锁

实现同步的一种重要工具,阻止其他线程同时访问。常用的锁包括互斥锁、读写锁等。

3、原子操作

原子操作是不可中断的操作,即在执行完毕之前不会被其他线程打断。对于多线程环境中的某些关键操作,如计数器的增加,使用原子操作可以确保线程安全。

4、避免race(竞态条件)

竞态条件是指两个或多个线程在访问共享数据时,由于它们的执行顺序不同而导致结果不确定的情况。通过仔细设计程序逻辑和使用同步机制,可以避免竞态条件的发生。

5、使用线程安全的类和数据结构

许多编程语言和库都提供了线程安全的类和数据结构,如Java中的java.util.concurrent包中的类。使用这些类可以简化线程安全的实现,减少出错的可能性。

6、线程间的交互

在设计多线程程序时,应仔细考虑线程间的交互方式。例如,可以通过消息传递、事件驱动等方式来协调线程间的操作,以确保线程安全

几种锁


1、常用的锁

  • 互斥锁
    • 同一时刻只 有一个线程可以访问(包含读写)资源
    • 问题: 加锁失败时,会用阻塞进入内核态,当再次加锁成功,一共会有两次线程切换的成本
  • 自旋锁:
    • 特殊的互斥锁,在加锁失败时不会使线程进入休眠,而是处于忙等待,让线程在循环中不断检查锁的状态
    • 适用:持锁时间短的场景,不会造成线程切换
  • 读写锁
    • 允许多个线程同时读,但只允许一个线程写
    • 适用:可以区分读和写的场景,能提高并发

2、乐观锁与悲观锁

  • 乐观锁:操作数据时认为别人不会修改数据,实现方式——
    • CAS(CompareAndSwap):仅数据是期望值时才修改。问题:
    • 需要额外的自旋重试机制,写法复杂且占用资源
    • 只能操作某一个变量,不适用于多个变量
    • ABA问题:① 线程 1 读取内存中数据为 A ②线程2将该数据修改为B ③线程2将该数据修改为A ④线程1对数据进行CAS操作。但使用栈时,虽然栈顶元素没变,但栈已经改变
    • 版本号机制:在数据中加一个version字段,数据有修改时+1,更新数据时,版本号一致才进行操作
  • 悲观锁
    • 操作数据时认为别人会修改数据,需要加锁
    • 互斥锁、自旋锁、读写锁都是悲观锁

3、死锁

原因:多个线程争夺资源
满足条件:互斥条件、占用等待条件、不可抢占、循环等待
如何避免:
  • 合理的资源分配
  • 避免嵌套锁
  • 按序获取资源:给资源编号,线程按顺序请求资源,按相反顺序释放资源
  • 超时放弃
  • 定期检查死锁

Mutex互斥锁


1、Mutex结构

type Mutex struct {
        state int32
        sema  uint32
}

state的32位表示:

  • mutexLocked — 最低1位,表示互斥锁的锁定状态;
  • mutexWoken — 低2位,表示从正常模式被从唤醒;
  • mutexStarving — 低3位,当前的互斥锁进入饥饿状态;
  • waitersCount — 其他位,当前互斥锁上等待的 Goroutine 个数;
底层实现:
  • 自旋:当锁被其他goroutine持有时,当前goroutine会进行一段时间的自旋(忙等待),而不是立即进入睡眠状态。以减少线程切换的开销
  • 阻塞:如果自旋一段时间后锁仍然被占用,goroutine会进入休眠,等待锁被释放。当锁被释放时时唤醒
  • 原子操作:底层使用了原子操作CAS,尝试获取锁或更新锁的状态
  • 公平性与非公平性:Mutex是非公平的,这意味着等待时间最长的goroutine并不一定会优先获得锁,锁有正常模式和饥饿模式

2、自旋的goroutine

如果一个G在时间片内没有完成任务,就会进入自旋状态(此时G依然持有P),如果自旋一段时间后仍然没有获得锁,那么该G对应的M就会释放cpu,并将G加入P的等待队列。

自旋可以避免 goroutine 的频繁切换,提高cpu性能

3、锁的两种模式

(1)正常模式

P的等待队列中的G会按照先进先出(FIFO)的顺序等待被唤醒。当一个G被唤醒时,并不能直接获得锁,而是要自己去抢占一次,但此时可能有其他G刚刚进入自旋流程,尝试获得锁,还没有加入阻塞队列。此时刚被唤醒的G与正在自旋的G竞争时,自旋的G更有优势,因为它正在占有cpu且数量更多。导致有部分G被饿死。

正常模式切换为饥饿模式:

  • 某个G超过1ms没有获取到锁

(2)饥饿模式

在饥饿模式下,想要获得这个锁的所有G都不会自旋等待,而是直接进入阻塞队列排队。那么锁就会直接被队头G获取。

饥饿模式切换回正常模式(满足其中之一):

  • 某个G获取到了锁且是队列中最后一个G,没有其他G等待了
  • 某个G的等待时间小于1ms

RWMutex读写锁


1、读写限制

不限制并发读,但不能并行执行 读写、写写

2、写锁

(1)加锁 Lock()
  • 先用mutex阻塞后续写操作
  • atomic控制读的数量,阻塞后续读操作
  • 如果有其他goroutine持有读锁,会休眠等待所有读锁释放
    先阻塞写锁的获取,再阻塞读锁的获取,可以保证读操作不会被连续的写操作饿死,
(2)释放锁 UnLock()
  • 释放读锁
  • 释放所有由于获取读锁而等待的goroutine
  • 释放写锁

3、读锁

(1)加锁 RLock()
    atomic将读的数量readerCount++
  • 如果readerCount<0——表明有其他goroutine持有写锁,会休眠等待写锁释放
  • 如果readerCount>=0——加锁成功
(2)释放锁 RUnLock()
    atomic将读的数量readerCount--
  • 如果readerCount>=0——解锁成功
  • 如果读readerCount<0——有其他goroutine持有写锁,会等待写锁释放

atomic原语


1、原子操作与互斥锁区别

目的:
  • atomic: 对某个变量的互斥操作,粒度小
  • mutex:  保护一段逻辑操作,粒度较大
底层实现:
  • atomic:  硬件实现,利用 CPU 的原子指令(例如 CAS Compare-And-Swap
  • mutex:  软件实现,依赖调度器 

2、方法

对  int32 int64 uint32 uint64 uintptr 类型的各个方法:
  • 增减操作( AddXXType ):对操作数的原子增减
  • 载入操作( LoadXXType ):读取到操作数前,没有其他 routine 对其进行更改操作
  • 存储操作( StoreXXType ):存储保证原子性(避免被其他线程读取到修改一半的数据)
  • 比较并交互操作( CompareAndSwapXXType ):保证交换的 CAS ,只有原有值没被更改时才会交换
  • 交换操作(SwapXXType):直接交换,不关心原有值

3、atomic.Value

Value 相当于一个容器,可用来原子存储或加载任意类型的值
存储有类型要求:
  • 不能存储nil(会panic)
  • value中存储的第一个值,决定了其后续的值类型
  • 尝试存储不同的类型,会panic
m := make(map[int]int)
var config atomic.Value

go func() {
    var i int
    for {
        m[i] = rand.Intn(100)
        i++

        config.Store(m)
        time.Sleep(1 * time.Millisecond * 500)
    }
}()

time.Sleep(time.Second * 2)
c := config.Load().(map[int]int)
t.Log(c)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值