专栏首页网管叨bi叨原子操作和互斥锁的区别

原子操作和互斥锁的区别

这个系列的文章里介绍了很多并发编程里经常用到的技术,除了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 main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {
    var n int32
      var wg sync.WaitGroup
      for 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 main

import (
    "fmt"
    "sync/atomic"
)

var value int32 = 1

func main()  {
    fmt.Println("======old value=======")
    fmt.Println(value)
    fmt.Println("======New value=======")
    fmt.Println(value)

}

//不断地尝试原子地更新value的值,直到操作成功为止
func addValue(delta int32){
    for {
        v := value
        if 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包的能力。

本文分享自微信公众号 - 网管叨bi叨(kevin_tech),作者:KevinYan11

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-06-10

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Laravel神奇的服务容器

    Laravel的核心是IocContainer, 文档中称其为“服务容器”,服务容器是一个用于管理类依赖和执行依赖注入的强大工具,Laravel中的功能模块比如...

    KevinYan
  • Laravel源码分析之Route

    路由是外界访问Laravel应用程序的通路或者说路由定义了Laravel的应用程序向外界提供服务的具体方式:通过指定的URI、HTTP请求方法以及路由参数(可选...

    KevinYan
  • Laravel源码解析之用户认证系统(二)

    上一节我们介绍了Laravel Auth系统的基础知识,说了他的核心组件都有哪些构成,这一节我们会专注Laravel Auth系统的实现细节,主要关注 Auth...

    KevinYan
  • [NewLife.XCode]分表分库(百亿级大数据存储)

    NewLife.XCode是一个有15年历史的开源数据中间件,支持netcore/net45/net40,由新生命团队(2002~2019)开发完成并维护至今,...

    大石头
  • SpringBoot常用注解详解

    所有的配置都是写在appliaction.properties或application.yml文件里,那么如果不想写在这里面怎么处理呢使用@PropertySo...

    Little JAVA
  • Django开发网页计数器统计页面浏览次数

    实际Web开发过程中,我们经常要统计并显示一个页面的浏览次数。今天我们会以博客的例子,教你如何利用Django开发网页计数器,统计并显示一篇文章的浏览次数。本文...

    菲宇
  • 那些一键抠图的软件是怎么做到的?这些语义分割方法了解一下

    在深度学习时代到来之前,大量的图像处理技术被用来将图像分割成一些感兴趣的区域(ROI)。下面列出了一些常用的方法。 灰度分割 这是最简单的语义分割形式,它包...

    机器之心
  • 锁定文件失败,打不开磁盘“D:\biehl\centos\taotao\taotao-dubbo-register\CentOS-000001.vmdk”或它所依赖的某个快照磁盘。模块“Disk”启动

    1、锁定文件失败,打不开磁盘“D:\biehl\centos\taotao\taotao-dubbo-register\CentOS-000001.vmdk”或...

    别先生
  • CNCF网研会(视频+PDF):YAML是可选的:探索应用程序开发者的Kubernetes选项

    这次演讲的重点是可用性,以及当前的工具,例如kubectl和YAML,如何不能满足应用程序开发者的目标。会谈到NetApp在ALM方面的工作,也会谈到生态系统的...

    CNCF
  • 制定自己团队的前端开发规范

    后续的文章我也补充齐全了,一共有两篇,都是实战篇,一篇是制定自己团队的前端开发规范之 eslint,另外一篇是手摸手带你实践标准的前端开发规范,希望大家可以去看...

    用户1462769

扫码关注云+社区

领取腾讯云代金券