目录
如何保证线程安全
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、写锁
-
先用mutex阻塞后续写操作
-
atomic控制读的数量,阻塞后续读操作
-
如果有其他goroutine持有读锁,会休眠等待所有读锁释放
-
释放读锁
-
释放所有由于获取读锁而等待的goroutine
-
释放写锁
3、读锁
-
如果readerCount<0——表明有其他goroutine持有写锁,会休眠等待写锁释放
-
如果readerCount>=0——加锁成功
-
如果readerCount>=0——解锁成功
-
如果读readerCount<0——有其他goroutine持有写锁,会等待写锁释放
atomic原语
1、原子操作与互斥锁区别
-
atomic: 对某个变量的互斥操作,粒度小
-
mutex: 保护一段逻辑操作,粒度较大
-
atomic: 硬件实现,利用 CPU 的原子指令(例如 CAS , Compare-And-Swap )
-
mutex: 软件实现,依赖调度器
2、方法
-
增减操作( AddXXType ):对操作数的原子增减
-
载入操作( LoadXXType ):读取到操作数前,没有其他 routine 对其进行更改操作
-
存储操作( StoreXXType ):存储保证原子性(避免被其他线程读取到修改一半的数据)
-
比较并交互操作( CompareAndSwapXXType ):保证交换的 CAS ,只有原有值没被更改时才会交换
-
交换操作(SwapXXType):直接交换,不关心原有值
3、atomic.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)