这个系列的文章里介绍了很多并发编程里经常用到的技术,除了Context、计时器、互斥锁还有通道外还有一种技术--原子操作在一些同步算法中会被用到。今天的文章里我们会简单了解一下Go语言里对原子操作的支持,然后探讨一下原子操作和互斥锁的区别。

文章的主要话题如下:

  • 原子操作

  • Go对原子操作的支持

  • 原子操作和互斥锁的区别

原子操作

原子操作即是进行过程中不能被中断的操作,针对某个值的原子操作在被进行的过程中,CPU绝不会再去进行其他的针对该值的操作。为了实现这样的严谨性,原子操作仅会由一个独立的CPU指令代表和完成。原子操作是无锁的,常常直接通过CPU指令直接实现。事实上,其它同步技术的实现常常依赖于原子操作。

Go对原子操作的支持

Go 语言的sync/atomic包提供了对原子操作的支持,用于同步访问整数和指针。

  • Go语言提供的原子操作都是非入侵式的。

  • 这些函数提供的原子操作共有五种:增减、比较并交换、载入、存储、交换。

  • 原子操作支持的类型类型包括int32、int64、uint32、uint64、uintptr、unsafe.Pointer。

下面的示例演示如何使用AddInt32函数对int32值执行添加原子操作。在这个例子中,main goroutine创建了1000个的并发goroutine。每个新创建的goroutine将整数n加1。

package mainimport ("fmt""sync""sync/atomic"
)func main() {var n int32var wg sync.WaitGroupfor i := 0; i < 1000; i++ {wg.Add(1)go func() {atomic.AddInt32(&n, 1)wg.Done()}()}wg.Wait()fmt.Println(atomic.LoadInt32(&n)) // output:1000
}

上面的例子里你们可以自己试验一下,如果我们不使用atomic.AddInt32(&n, 1)而是简单的对变量n进行自增的话得到结果并不是我们预期的1000,这就是我们在文章《Go并发编程里的数据竞争以及解决之道》里提到过的数据竞争问题,原子操作可确保这些goroutine之间不存在数据竞争。

原子操作中的比较并交换简称CAS(Compare And Swap),在sync/atomic包中,这类原子操作由名称以CompareAndSwap为前缀的若干个函数提供

func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer,old, new unsafe.Pointer) (swapped bool)
......

调用函数后,CompareAndSwap函数会先判断参数addr指向的操作值与参数old的值是否相等,仅当此判断得到的结果是true之后,才会用参数new代表的新值替换掉原先的旧值,否则操作就会被忽略。

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

package mainimport ("fmt""sync/atomic"
)var value int32 = 1func main()  {fmt.Println("======old value=======")fmt.Println(value)fmt.Println("======New value=======")fmt.Println(value)}//不断地尝试原子地更新value的值,直到操作成功为止
func addValue(delta int32){for {v := valueif atomic.CompareAndSwapInt32(&value, v, (v + delta)){break}}
}

上面的比较并交换案例中 v:= value为变量v赋值,但要注意,在进行读取value的操作的过程中,其他对此值的读写操作是可以被同时进行的,那么这个读操作很可能会读取到一个只被修改了一半的数据。所以 我们要使用sync/atomic代码包中为我们提供的以Load为前缀的函数,来避免这样的糟糕事情发生。

竞争条件是由于异步的访问共享资源,并试图同时读写该资源而导致的,使用互斥锁和通道的思路都是在线程获得到访问权后阻塞其他线程对共享内存的访问,而使用原子操作解决数据竞争问题则是利用了其不可被打断的特性。

关于atomic包更详细的使用介绍可以访问官方的sync/atomic中文文档:https://go-zh.org/pkg/sync/atomic/

原子操作与互斥锁的区别

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

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

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

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

  • 互斥锁是一种数据结构,用来让一个线程执行程序的关键部分,完成互斥的多个操作。

  • 原子操作是针对某个值的单个互斥操作。

  • 可以把互斥锁理解为悲观锁,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。

atomic包提供了底层的原子性内存原语,这对于同步算法的实现很有用。这些函数一定要非常小心地使用,使用不当反而会增加系统资源的开销,对于应用层来说,最好使用通道或sync包中提供的功能来完成同步操作。

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

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

推荐阅读:

上周并发题的解题思路以及介绍Go语言调度器

原子操作和互斥锁的区别相关推荐

  1. Go语言的原子操作和互斥锁的区别

    这个系列的文章里介绍了很多并发编程里经常用到的技术,除了Context.计时器.互斥锁还有通道外还有一种技术--原子操作在一些同步算法中会被用到.今天的文章里我们会简单了解一下Go语言里对原子操作的支 ...

  2. 原子操作和互斥量的区别

    原子操作和互斥量的区别 原子操作和互斥锁都是并发编程中常见的技术. 原子操作 原子操作就是操作过程中不能被中断的过程,在针对某个值得原子操作,在被进行的过程中CPU绝对不会再进行其他的针对该值的操作. ...

  3. 什么是自旋锁+自旋锁和互斥锁的区别

    文章目录 本文链接 什么是自旋锁 参考链接 自旋锁和互斥锁的区别 参考链接 本文链接 击打 什么是自旋锁 多线程对共享资源访问, 为防止并发引起的相关问题, 常引入锁的机制来处理并发问题.   获取到 ...

  4. 自旋锁与互斥锁的区别,信号量和互斥锁的区别

    文章目录 自旋锁与互斥锁的区别 信号量和互斥锁的区别 什么是信号量 什么是互斥锁 主要区别 生产者-消费者问题 使用mutex 使用信号量 信号量的优点 mutex的优点 信号量的缺点 mutex的缺 ...

  5. 学习笔记(26):Python网络编程并发编程-GIL与自定义互斥锁的区别

    立即学习:https://edu.csdn.net/course/play/24458/296443?utm_source=blogtoedu 1.GIL的基本概念 答:GIL本质上就是一把锁,只是他 ...

  6. 自旋锁和互斥锁的区别

    自旋锁和互斥锁的区别 POSIX threads(简称Pthreads)是在多核平台上进行并行编程的一套API.线程同步是并行编程中非常重要的通讯手段,其中最典型的应用就是用 Pthreads提供的锁 ...

  7. 自旋锁与互斥锁的区别

    自旋锁和互斥锁的区别 POSIX threads(简称Pthreads)是在多核平台上进行并行编程的一套API.线程同步是并行编程中非常重要的通讯手段,其中最典型的应用就是用 Pthreads提供的锁 ...

  8. linux进程--自旋锁和互斥锁的区别(十四)

    自旋锁(Spin lock) 自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是 否该自旋锁的保持者已经释放了锁,"自旋&qu ...

  9. 信号量与线程互斥锁的区别

    援引CU上一篇帖子的内容: "信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都在semtake的时候,就阻塞在 哪里).而互斥锁 ...

最新文章

  1. Grafana常用的监控
  2. host文件修改后无法保存的问题
  3. SSD 通俗易懂介绍
  4. 一文学会设置 Jupyter 主题与目录
  5. 特征工程(part5)--分类型变量
  6. C++_虚继承_虚函数_纯虚函数(多继承的二义性,多态)
  7. 华硕固件,梅林固件,高恪固件等等有什么区别?
  8. log4j.properties文件示例
  9. java 线程重入,java synchronized加载加锁-线程可重入详解及实例代码
  10. linux怎样判断线程是否暂停_怎样寻找合适的创业项目?如何判断一个创业项目是否靠谱?...
  11. Java--ArrayList的遍历
  12. Hadoop-Streaming实战经验及问题解决方法总结
  13. leetcode 打印_leetcode多线程之按序打印
  14. 最新最全latex在sublime上的配置步骤全解
  15. 小学生机器人挑战赛_适合小学生参加的机器人比赛有哪些?
  16. Maven父子项目的理解
  17. 打地鼠游戏(2D)学习笔记
  18. CSS选择器 :first-of-type/:last-of-type/ :first-child/:last-child 用法
  19. 互补品的需求曲线图_如图,D是某商品的需求曲线,当该商品的互补品价格下降时,则该商品的需求曲线会出现移动。下列各图(横轴为需求量,纵轴为价格)能够正确反映这一变化的是...
  20. 快牙网传——推送通知

热门文章

  1. Redis(三)、支持数据类型及常用操作命令
  2. Spring Boot系列(十二)Spring Boot整合ActiveQ实现消息收发和订阅
  3. mysql-bin日志文件清理
  4. LeetCode OJ - Sort List
  5. she's gone
  6. 本地wamp的Internal Server Error错误解决方法
  7. (转载)查看Oracle字符集及怎样修改字符集
  8. PHP_TP5框架开发后端接口(代码编写思路)
  9. vue学习笔记(五):对于vuex的理解 + 简单实例
  10. ESLint使用文档