原子操作,在多线程编程中是一个很常见的课题,指的是一个操作或一系列操作在被CPU调度的时候不可中断。早期的软件基本都是单核单线程,每个操作都可以视为原子操作,因此不会有并发问题,但随着现在多核多线程编程的出现,线程并发成为了多线程编程中不可回避的一个课题。
从硬件层面来实现原子操作,有两种方式:
1、总线加锁:因为CPU和其他硬件的通信都是通过总线控制的,所以可以通过在总线加LOCK#锁的方式实现原子操作,但这样会阻塞其他硬件对CPU的访问,开销比较大。
2、缓存锁定:频繁使用的内存会被处理器放进高速缓存中,那么原子操作就可以直接在处理器的高速缓存中进行而不需要使用总线锁,主要依靠缓存一致性来保证其原子性。
Golang提供了了一套原子操作的接口,可以在sync\atomic目录下查看,在里面我们可以看到经典的CAS函数。CAS即比较及交换,以 CompareAndSwapInt64 这个函数为例:
// CompareAndSwapInt64 executes the compare-and-swap operation for an int64 value. func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
这个函数有三个参数,第一个是目标数据的地址,第二个是目标数据的旧值,第三个则是等待更新的新值。每次CAS都会用old和addr内的数据进行比较,如果数值相等,则执行操作,用new覆盖addr内的旧值,如果数据不相等,则忽略后面的操作。在高并发的情况下,单次CAS的执行成功率会降低,因此需要配合循环语句for,形成一个for+atmoc的类似自旋乐观锁的操作。下面是测试代码:
package main import ( "fmt" "sync" "sync/atomic" "time" ) var G_Int int64; var WG sync.WaitGroup; var ThreadCnt int func AtmoicOpr() { var TempInt int64 for { //等待所有协程启动完成,模拟并发问题 if ThreadCnt == 100 { break; } } //错误写法 //TempInt = G_Int //Result := atomic.CompareAndSwapInt64(&G_Int, TempInt, TempInt + 1) //fmt.Println(TempInt, " Try to CAS: " ,Result) //正确写法 for { TempInt = atomic.LoadInt64(&G_Int) Result := atomic.CompareAndSwapInt64(&G_Int, TempInt, TempInt + 1) if Result == true { fmt.Println(TempInt, " Try to CAS: " ,Result) break; } } WG.Done() } func main() { G_Int = 0 ThreadCnt = 0 for i:= 0; i< 100; i++ { go AtmoicOpr() WG.Add(1) ThreadCnt += 1 fmt.Println("ThreadCnt is: ", ThreadCnt) } WG.Wait() time.Sleep(time.Second * 2) }
原子操作主要由硬件提供支持,锁一般是由操作系统提供支持,比起直接使用锁,使用CAS这个过程不需要形成临界区和创建互斥量,所以会比使用锁更加高效。
https://blog.csdn.net/qq_39920531/article/details/97646901
最新评论: