Go语言原子操作及互斥锁的区别

目录
  • 增或减
  • 比较并交换(Compare And Swap)
  • 载入与存储
  • 交换
  • 原子值
  • 原子操作与互斥锁的区别

原子操作就是不可中断的操作,外界是看不到原子操作的中间状态,要么看到原子操作已经完成,要么看到原子操作已经结束。在某个值的原子操作执行的过程中,CPU绝对不会再去执行其他针对该值的操作,那么其他操作也是原子操作。

Go语言中提供的原子操作都是非侵入式的,在标准库代码包sync/atomic中提供了相关的原子函数。

增或减

用于增或减的原子操作的函数名称都是以"Add"开头的,后面跟具体的类型名,比如下面这个示例就是int64类型的原子减操作

func main() {
   var  counter int64 =  23
   atomic.AddInt64(&counter,-3)
   fmt.Println(counter)
}
---output---
20

原子函数的第一个参数都是指向变量类型的指针,是因为原子操作需要知道该变量在内存中的存放位置,然后加以特殊的CPU指令,也就是说对于不能取得内存存放地址的变量是无法进行原子操作的。第二个参数的类型会自动转换为与第一个参数相同的类型。此外,原子操作会自动将操作后的值赋值给变量,无需我们自己手动赋值了。

对于 atomic.AddUint32() 和 atomic.AddUint64() 的第二个参数为 uint32 与 uint64,因此无法直接传递一个负的数值进行减法操作,Go语言提供了另一种方法来迂回实现:使用二进制补码的特性

注意:unsafe.Pointer 类型的值无法被加减。

比较并交换(Compare And Swap)

简称CAS,在标准库代码包sync/atomic中以”Compare And Swap“为前缀的若干函数就是CAS操作函数,比如下面这个

func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)

第一个参数的值是这个变量的指针,第二个参数是这个变量的旧值,第三个参数指的是这个变量的新值。

运行过程:调用CompareAndSwapInt32 后,会先判断这个指针上的值是否跟旧值相等,若相等,就用新值覆盖掉这个值,若相等,那么后面的操作就会被忽略掉。返回一个 swapped 布尔值,表示是否已经进行了值替换操作。

与锁有不同之处:锁总是假设会有并发操作修改被操作的值,而CAS总是假设值没有被修改,因此CAS比起锁要更低的性能损耗,锁被称为悲观锁,而CAS被称为乐观锁。

CAS的使用示例

var value int32
func AddValue(delta int32)  {
   for {
      v:= value
      if atomic.CompareAndSwapInt32(&value,v,(v+delta)) {
         break
      }
   }
}

由示例可以看出,我们需要多次使用for循环来判断该值是否已被更改,为了保证CAS操作成功,仅在 CompareAndSwapInt32 返回为 true时才退出循环,这跟自旋锁的自旋行为相似。

载入与存储

对一个值进行读或写时,并不代表这个值是最新的值,也有可能是在在读或写的过程中进行了并发的写操作导致原值改变。为了解决这问题,Go语言的标准库代码包sync/atomic提供了原子的读取(Load为前缀的函数)或写入(Store为前缀的函数)某个值

将上面的示例改为原子读取

var value int32
func AddValue(delta int32)  {
   for {
      v:= atomic.LoadInt32(&value)
      if atomic.CompareAndSwapInt32(&value,v,(v+delta)) {
         break
      }
   }
}

原子写入总会成功,因为它不需要关心原值是什么,而CAS中必须关注旧值,因此原子写入并不能代替CAS,原子写入包含两个参数,以下面的StroeInt32为例:

//第一个参数是被操作值的指针,第二个是被操作值的新值
func StoreInt32(addr *int32, val int32)

交换

这类操作都以”Swap“开头的函数,称为”原子交换操作“,功能与之前说的CAS操作与原子写入操作有相似之处。

func SwapInt32(addr *int32, new int32) (old int32)

以 SwapInt32 为例,第一个参数是int32类型的指针,第二个是新值。原子交换操作不需要关心原值,而是直接设置新值,但是会返回被操作值的旧值。

原子值

Go语言的标准库代码包sync/atomic中有一个叫做Value的原子值,它是一个结构体类型,用于存储需要原子读写的值,结构体如下

// Value提供原子加载并存储一致类型的值。
// Value的零值从Load返回nil。
//调用Store后,不得复制值。
//首次使用后不得复制值。
type Value struct {
   v interface{}
}

可以看出结构体内是一个 v interface{},也就是说 该Value原子值可以保存任何类型的需要原子读写的值。

使用方式如下:

var Atomicvalue  atomic.Value

该类型有两个公开的指针方法

//原子的读取原子值实例中存储的值,返回一个 interface{} 类型的值,且不接受任何参数。
//若未曾通过store方法存储值之前,会返回nil
func (v *Value) Load() (x interface{})

//原子的在原子实例中存储一个值,接收一个 interface{} 类型(不能为nil)的参数,且不会返回任何值
func (v *Value) Store(x interface{})

一旦原子值实例存储了某个类型的值,那么之后Store存储的值就必须是与该类型一致,否则就会引发panic。

严格来讲,atomic.Value类型的变量一旦被声明,就不应该被复制到其他地方。比如:作为源值赋值给其他变量,作为参数传递给函数,作为结果值从函数返回,作为元素值通过通道传递,这些都会造成值的复制。

但是atomic.Value类型的指针类型变量就不会存在这个问题,原因是对结构体的复制不但会生成该值的副本,还会生成其中字段的副本,这样那么并发引发的值变化都与原值没关系了。

看下面这个小示例

func main() {
   var Atomicvalue  atomic.Value
   Atomicvalue.Store([]int{1,2,3,4,5})
   anotherStore(Atomicvalue)
   fmt.Println("main: ",Atomicvalue)
}

func anotherStore(Atomicvalue atomic.Value)  {
   Atomicvalue.Store([]int{6,7,8,9,10})
   fmt.Println("anotherStore: ",Atomicvalue)
}
---output---
anotherStore:  {[6 7 8 9 10]}
main:  {[1 2 3 4 5]}

原子操作与互斥锁的区别

互斥锁是一种数据结构,使你可以执行一系列互斥操作。而原子操作是互斥的单个操作,这意味着没有其他线程可以打断它。那么就Go语言里atomic包里的原子操作和sync包提供的同步锁有什么不同呢?

首先atomic操作的优势是更轻量,比如CAS可以在不形成临界区和创建互斥量的情况下完成并发安全的值替换操作。这可以大大的减少同步对程序性能的损耗。

原子操作也有劣势。还是以CAS操作为例,使用CAS操作的做法趋于乐观,总是假设被操作值未曾被改变(即与旧值相等),并一旦确认这个假设的真实性就立即进行值替换,那么在被操作值被频繁变更的情况下,CAS操作并不那么容易成功。而使用互斥锁的做法则趋于悲观,我们总假设会有并发的操作要修改被操作的值,并使用锁将相关操作放入临界区中加以保护。

所以总结下来原子操作与互斥锁的区别有:

互斥锁是一种数据结构,用来让一个线程执行程序的关键部分,完成互斥的多个操作。
原子操作是针对某个值的单个互斥操作。
可以把互斥锁理解为悲观锁,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
atomic包提供了底层的原子性内存原语,这对于同步算法的实现很有用。这些函数一定要非常小心地使用,使用不当反而会增加系统资源的开销,对于应用层来说,最好使用通道或sync包中提供的功能来完成同步操作。

针对atomic包的观点在Google的邮件组里也有很多讨论,其中一个结论解释是:

应避免使用该包装。或者,阅读C ++ 11标准的“原子操作”一章;如果您了解如何在C ++中安全地使用这些操作,那么你才能有安全地使用Go的 sync/atomic包的能力。

到此这篇关于Go语言原子操作及互斥锁的区别 的文章就介绍到这了,更多相关Go语言原子操作内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Golang使用lua脚本实现redis原子操作

    目录 [redis 调用Lua脚本](#redis 调用Lua脚本) [redis+lua 实现评分排行榜实时更新](#redis+lua 实现评分排行榜实时更新) [lua 脚本](#lua 脚本) Golang调用redis+lua示例 byte切片与string的转换优化 redis 调用Lua脚本 EVAL命令 redis调用Lua脚本需要使用EVAL命令. redis EVAL命令格式: redis 127.0.0.1:6379> EVAL script numkeys key [ke

  • 详解Golang五种原子性操作的用法

    目录 Go 语言提供了哪些原子操作 互斥锁跟原子操作的区别 比较并交换 atomic.Value保证任意值的读写安全 总结 本文我们详细聊一下Go语言的原子操作的用法,啥是原子操作呢?顾名思义,原子操作就是具备原子性的操作... 是不是感觉说了跟没说一样,原子性的解释如下: 一个或者多个操作在 CPU 执行的过程中不被中断的特性,称为原子性(atomicity) .这些操作对外表现成一个不可分割的整体,他们要么都执行,要么都不执行,外界不会看到他们只执行到一半的状态. CPU执行一系列操作时不可

  • Go语言原子操作及互斥锁的区别

    目录 增或减 比较并交换(Compare And Swap) 载入与存储 交换 原子值 原子操作与互斥锁的区别 原子操作就是不可中断的操作,外界是看不到原子操作的中间状态,要么看到原子操作已经完成,要么看到原子操作已经结束.在某个值的原子操作执行的过程中,CPU绝对不会再去执行其他针对该值的操作,那么其他操作也是原子操作. Go语言中提供的原子操作都是非侵入式的,在标准库代码包sync/atomic中提供了相关的原子函数. 增或减 用于增或减的原子操作的函数名称都是以"Add"开头的,

  • Go语言底层原理互斥锁的实现原理

    目录 Go 互斥锁的实现原理? 概念 使用场景 底层实现结构 操作 加锁 解锁 Go 互斥锁正常模式和饥饿模式的区别? 正常模式(非公平锁) 饥饿模式(公平锁) Go 互斥锁允许自旋的条件? Go 互斥锁的实现原理? Go sync包提供了两种锁类型:互斥锁sync.Mutex 和 读写互斥锁sync.RWMutex,都属于悲观锁. 概念 Mutex是互斥锁,当一个 goroutine 获得了锁后,其他 goroutine 不能获取锁(只能存在一个写者或读者,不能同时读和写) 使用场景 多个线程

  • Go语言并发编程 互斥锁详情

    目录 1.互斥锁Mutex 1.1 Mutex介绍 1.2 Mutex使用实例 2.读写锁RWMutex 2.1 RWMutex介绍 2.2 RWMutex使用实例 1.互斥锁Mutex 1.1 Mutex介绍 Go 语言的同步工具主要由 sync 包提供,互斥锁 (Mutex) 与读写锁 (RWMutex) 就是sync 包中的方法. 互斥锁可以用来保护一个临界区,保证同一时刻只有一个 goroutine 处于该临界区内.主要包括锁定(Lock方法)和解锁(Unlock方法)两个操作,首先对进

  • GO语言协程互斥锁Mutex和读写锁RWMutex用法实例详解

    sync.Mutex Go中使用sync.Mutex类型实现mutex(排他锁.互斥锁).在源代码的sync/mutex.go文件中,有如下定义: // A Mutex is a mutual exclusion lock. // The zero value for a Mutex is an unlocked mutex. // // A Mutex must not be copied after first use. type Mutex struct { state int32 sem

  • GoLang中的互斥锁Mutex和读写锁RWMutex使用教程

    目录 一.竞态条件与临界区和同步工具 (1)竞态条件 (2)临界区 (3)同步工具 二.互斥量 三.使用互斥锁的注意事项 (1)使用互斥锁的注意事项 (2)使用defer语句解锁 (3)sync.Mutex是值类型 四.读写锁与互斥锁的异同 (1)读/写互斥锁 (2)读写锁规则 (3)解锁读写锁 一.竞态条件与临界区和同步工具 (1)竞态条件 一旦数据被多个线程共享,那么就会产生冲突和争用的情况,这种情况被称为竞态条件.这往往会破坏数据的一致性. 同步的用途有两个,一个是避免多线程在同一时刻操作

  • Go语言线程安全之互斥锁与读写锁

    目录 一.互斥锁是什么? 1.概念 2.未加锁 3.加锁之后 二.读写锁[效率革命] 1.为什么读写锁效率高 2.使用方法 三.sync.once 1.sync.once产生背景 2.sync.once机制概述 3.sync.once注意点 4.使用方法 四.atomic原子包操作 前言: 单个线程时数据操作的只有一个线程,数据的修改也只有一个线程参与,数据相对来说是安全的,多线程时对数据操作的不止一个线程,所以同时对数据进行修改的时候难免紊乱 一.互斥锁是什么? 1.概念 互斥锁是为了并发的安

  • Go语言的互斥锁的详细使用

    目录 前言 Go语言互斥锁设计实现 mutex介绍 Lock加锁 初始化状态 自旋 抢锁准备期望状态 通过CAS操作更新期望状态 解锁 非阻塞加锁 总结 前言 当提到并发编程.多线程编程时,都会在第一时间想到锁,锁是并发编程中的同步原语,他可以保证多线程在访问同一片内存时不会出现竞争来保证并发安全:在Go语言中更推崇由channel通过通信的方式实现共享内存,这个设计点与许多主流编程语言不一致,但是Go语言也在sync包中提供了互斥锁.读写锁,毕竟channel也不能满足所有场景,互斥锁.读写锁

  • 一文掌握Go语言并发编程必备的Mutex互斥锁

    目录 1. Mutex 互斥锁的基本概念 2. Mutex 互斥锁的基本用法 3. Mutex 互斥锁的底层实现 3.1 等待队列 3.2 锁状态 4. Mutex 互斥锁的注意事项 4.1 不要将 Mutex 作为函数或方法的参数传递 4.2 不要在获取 Mutex 的锁时阻塞太久 4.3 不要重复释放 Mutex 的锁 4.4 不要在锁内部执行阻塞或耗时操作 5. 总结 在并发编程中,我们需要处理多个线程同时对共享资源的访问问题.如果不加控制地同时访问共享资源,就会导致竞争条件(Race C

  • GO语言并发编程之互斥锁、读写锁详解

    在本节,我们对Go语言所提供的与锁有关的API进行说明.这包括了互斥锁和读写锁.我们在第6章描述过互斥锁,但却没有提到过读写锁.这两种锁对于传统的并发程序来说都是非常常用和重要的. 一.互斥锁 互斥锁是传统的并发程序对共享资源进行访问控制的主要手段.它由标准库代码包sync中的Mutex结构体类型代表.sync.Mutex类型(确切地说,是*sync.Mutex类型)只有两个公开方法--Lock和Unlock.顾名思义,前者被用于锁定当前的互斥量,而后者则被用来对当前的互斥量进行解锁. 类型sy

  • Go语言实现互斥锁、随机数、time、List

    Go语言实现互斥锁.随机数.time.List import ( "container/list" "fmt" "math/rand" //备注2:随机数的包 "sync" //备注1:异步任务的包 "time" ) type INFO struct { lock sync.Mutex //备注1:异步锁 Name string Time int64 } var List *list.List = list

随机推荐