前言

Go 源码版本:1.16
如果本文对你有帮助,给个赞吧;
喜欢本文就收藏一下吧;
有问题欢迎评论留言,基本都会回。

1. 引入

下面这段程序输出是多少呢?

package main

import (

"fmt"

"time"

) var a int = 0
func main() {

for i := 0; i < 1000; i++ {

go func() {

a++

}()

}

fmt.Printf("a = %d\n", a)

time.Sleep(time.Second)

}

在 main 协程中开 1000 个协程,每个协程都是对全局变量 a 进行自加操作,看起来输出应该会是 1000,但你可以多运行几次,你会发现输出几乎没有 1000!
根本原因是 a++ 并不是原子操作。
a++ 从开始到完成可以分成 3 步:

  1. 从主存读出 a 送到 CPU;
  2. CPU 进行 a = a + 1
  3. CPU 将新的 a 写回主存。

这样一来 1000 个协程并发执行时有可能读到相同的 a 值,也就是说每个协程读到的 a 可能不是期望中最新的 a(第 2 个协程期望读到最新的 a = 1

2. sync.atomic 原子操作

2.1 什么是原子操作

为了得到 1000,我们必须让以上 3 步在执行时一气呵成,不可中断,要么都执行了,要么都没执行。
从上面这句话引出原子性和原子操作的概念:

  1. 原子性:一个或多个操作在 CPU 执行过程中不可中断的特性。
  2. 原子操作:不会被线程调度打断的操作。

因此原子操作从表现上看,就是一气呵成,不可中断,要么都执行了,要么都没执行。

那么原子操作是如何实现的呢?
一顿搜索操作后,我知道了,作为程序员,仅需要知道是通过底层硬件支持实现的即可。
(我个人的理解是根据硬件特性编写的一段汇编代码来实现的,因此不同的 CPU 架构的实现略有差异)

当我们打开 $GOROOT/src/sync/atomic 下的查看源码时,发现只有几个函数,并没有具体实现,如下图:

但是我们看到 asm.s 文件里,好像当我们调用上图的 API 时,会跳到 runtime/internal/atomic 里去执行。

那么就打开看看里面有什么。我随便打开了一个 amd64 架构的版本,出现在我眼前的就是这一段汇编代码!

2.2 各种 API 的作用

作为一个程序员,我实在是不想知道底层硬件是如何实现原子操作的,我只会调 API(哎,有脑子就是不用,我就是玩= =)。

我们可以将 API 分为 5 大类:

  1. StoreXXX:存储操作
  2. LoadXXX:加载操作
  3. AddXXX:加法操作
  4. SwapXXX:交换操作
  5. CompareAndSwapXXX:比较与交换操作

其中 XXX 代表 API 支持的各种数据类型,从函数名上我们很容易看出,以上 5 大类操作都支持 6 种数据类型:int32, int64, uint32, uint64, uintptr 和 unsafe.Pointer(唯一例外的是 Add 操作不支持 unsafe.Pointer 类型)
前面 4 种类型都很容易理解,后面 2 种的意思如下:

// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr

无情翻译:uintptr 是一种足够大、能容纳任何长度比特模式的指针的整型。

type ArbitraryType int
type Pointer *ArbitraryType
// int is a signed integer type that is at least 32 bits in size. It is a

// distinct type, however, and not an alias for, say, int32.

type int int

无情翻译:unsafe.Pointer 其实就是 *int 的别名,而在 go 中 int 是至少 32 位、可变长度的有符号整型(易知最长是 64 位= =)

以下内容引自:atomic 包的使用及解析
Go 语言并不支持直接操作内存,但是它的标准库提供一种不保证向后兼容的指针类型 unsafe.Pointer,让程序可以灵活的操作内存,它的特别之处在于:可以绕过 Go 语言类型系统的检查。
也就是说:如果两种类型具有相同的内存结构,我们可以将 unsafe.Pointer 当作桥梁,让这两种类型的指针相互转换,从而实现同一份内存拥有两种解读方式。
例如 int 类型和 int32 类型内部的存储结构是一致的,但是对于指针类型的转换需要这么做:

var a int32
// 获得a的*int类型指针
(*int)(unsafe.Pointer(&a))

2.2.1 Store 操作

// StoreInt32 atomically stores val into *addr.
func StoreInt32(addr *int32, val int32)

将 val 存储到地址 addr 指向的内存单元,用一行代码表示它的作用就是 *addr = val
例子:将 4 存储到变量 a 中

var a int32 = 0
atomic.StoreInt32(&a, 4)

2.2.2 Load 操作

// LoadInt32 atomically loads *addr.
func LoadInt32(addr *int32) (val int32)

读取地址 addr 所指向的内存单元中的内容并返回。
例子:读取变量 a 的值

 var a int32 = 0
atomic.LoadInt32(&a)

2.2.3 Add 操作

// AddInt32 atomically adds delta to *addr and returns the new value.
func AddInt32(addr *int32, delta int32) (new int32)

将 delta 加到 addr 所指内存单元中并返回新的结果。
例子:将变量 a=0 加 2 之后返回新结果

var a int32 = 0
fmt.Println(atomic.AddInt32(&a, 2)) // 2

2.2.4 Swap 操作

// SwapInt32 atomically stores new into *addr and returns the previous *addr value.
func SwapInt32(addr *int32, new int32) (old int32)

将 new 存储到 addr 所指内存单元中,并返回内存单元中的旧值。
例子:将变量 a=1 改为 3

var a int32 = 1
fmt.Println(atomic.SwapInt32(&a, 3)) // 返回旧值 1
fmt.Println(a) // 新值 3

2.2.5 CompareAndSwap 操作

// CompareAndSwapInt32 executes the compare-and-swap operation for an int32 value.
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)

用代码来表示作用:

if *addr == old {
*addr = new
return true
} else {
return false
}

例子:

var a int32 = 1
// 因为 a 原本是 1,比较失败不交换,输出 false
fmt.Println(atomic.CompareAndSwapInt32(&a, 2, 3)) // false
fmt.Println(a) // 比较失败,没发生交换,因此还是 1
fmt.Println(atomic.CompareAndSwapInt32(&a, 1, 3)) // true
fmt.Println(a) // 3

注:CompareAndSwap 简称 CAS 操作。

3. atomic.Value 解析

前面翻译了一下常用的 5 大原子操作的作用,但在 atomic 包内还有一个 atomic.Value 类型,这是用来做什么的呢?

type Value struct {
v interface{}
}

从内部 v 的类型 interface{} 可以看出这是为了扩大存储范围,支持任意类型的存储操作,源码注释说明如下:

// A Value provides an atomic load and store of a consistently typed value.

无情翻译:Value 提供任意一致性类型值的原子 load 和 store 操作。

3.1 API 作用

// Store sets the value of the Value to x.
// All calls to Store for a given Value must use values of the same concrete type.
// Store of an inconsistent type panics, as does Store(nil).
func (v *Value) Store(x interface{})

无情翻译:Store 操作把 x 存储到 v,要求所有的 Store 调用传入的 x 的类型是一致的,若存储不一致类型的值,会直接 panic。

// Load returns the value set by the most recent Store.
// It returns nil if there has been no call to Store for this Value.
func (v *Value) Load() (x interface{})

无情翻译:Load 操作会返回最近一次 Store 存储的值。如果 v 还没通过 Store 存储过东西,会返回 nil。

源码分析参考:atomic 包的使用及解析

最后

如果你有疑惑,欢迎评论,我会尽可能回复!

如果本文对你有帮助,点个赞吧,这是我坚持的动力!

sync/atomic 原子操作使用与解析的更多相关文章

  1. golang sync/atomic

    刚刚学习golang原子操作处理的时候发现github上面一个比较不错的golang学习项目 附上链接:https://github.com/polaris1119/The-Golang-Standa ...

  2. 原子操作--sync/atomic的用法

    golang 通过sync/atomic库来支持cpu和操作系统级别的原子操作.但是对要操作类型有如下要求 int32, int64,uint32, uint64,uintptr,unsafe包中的P ...

  3. atomic 原子操作

    原子操作:操作仅由一个独立的CPU指令代表和完成.保证并发环境下原子操作的绝对安全 标准库代码包:sync/atomic atomic是最轻量级的锁,在一些场景下直接使用atomic包还是很有效的 C ...

  4. goalng-sync/atomic原子操作

    目录 1.go已经提供了锁,为什么还需要atomic原子操作? 2.atomic原子操作为什么比mutex快? 3.CAS 4.互斥锁与原子操作区别 5.原子操作方法 5.1 atomic.AddIn ...

  5. 并发之java.util.concurrent.atomic原子操作类包

    15.JDK1.8的Java.util.concurrent.atomic包小结 14.Java中Atomic包的原理和分析 13.java.util.concurrent.atomic原子操作类包 ...

  6. golang语言中sync/atomic包的学习与使用

    package main; import ( "sync/atomic" "fmt" "sync" ) //atomic包提供了底层的原子级 ...

  7. C++11开发中的Atomic原子操作

    C++11开发中的Atomic原子操作 Nicol的博客铭 原文  https://taozj.org/2016/09/C-11%E5%BC%80%E5%8F%91%E4%B8%AD%E7%9A%84 ...

  8. 并发之ATOMIC原子操作--Unsafe解析(三)

    Atomic 类的原子操作是依赖java中的魔法类sun.misc.Unsafe来实现的,而这个类为我们提供了访问底层的机制,这种机制仅供java核心类库使用,而不应该被普通用户使用. 获取Unsaf ...

  9. 深入理解Atomic原子操作和volatile非原子性

    原子操作可以理解为: 一个数,很多线程去同时修改它,不加sync同步锁,就可以保证修改结果是正确的 Atomic正是采用了CAS算法,所以可以在多线程环境下安全地操作对象. volatile是Java ...

  10. atomic原子操作

    C++中对共享数据的存取在并发条件下可能会引起data race的未定义行为,需要限制并发程序以某种特定的顺序执行,有两种方式:1.使用mutex保护共享数据: 2.原子操作 原子操作:针对原子类型操 ...

随机推荐

  1. 死磕面试系列,Java到底是值传递还是引用传递?

    Java到底是值传递还是引用传递? 这虽然是一个老生常谈的问题,但是对于没有深入研究过这块,或者Java基础不牢的同学,还是很难回答得让人满意. 可能很多同学能够很轻松的背出JVM.分布式事务.高并发 ...

  2. Docker | 专栏文章整理🎉🎉

    Docker Docker系列文章基本已经更新完毕,这是我从去年的学习笔记中整理出来的. 笔记稍微有点杂乱.随意,把它们整理成文章花费了不少力气.整理的过程也是我的一个再次学习的过程,同时也是为了方便 ...

  3. JSP利用AJAX实现页面即时校验验证码

    在JSP页面实现验证码校验文章中当时是使用的Servlet类来进行的验证码校验,但是这种方式并不能即时校验,在正常情况下都是直接在用户输入之后就进行校验,这样对用户来说很方便的. AJAX 即&quo ...

  4. i春秋Vld

    进去就问我们懂不懂Vulcan Logic Dumper,然后下面是一个报false.我们查看源码,在源码的最后提示我们index.php.txt的存在,话不多说,直接访问试试. 出现一堆我们暂时还看 ...

  5. Encodings: URL

    原题链接:http://www.wechall.net/challenge/training/encodings/url/index.php 题目信息:你的任务就是解码下面的东西.... 这个完全没难 ...

  6. Linux 基础-文件权限与属性

    一,文件类型 1.1,概述 1.2,正规文件(regular file) 1.3,目录(directory) 1.4,链接文件(link) 1.5,设备与装置文件(device) 1.6,资料接口文件 ...

  7. Django基础笔记5(Session)

    Session cookie:保存在客户端浏览器上的键值对 session:保存在服务器端的数据       保持会话 def index(req): v = req.session.get('use ...

  8. 【zookeeper】Zookeeper相关概念、重难点(myid)、语法、使用、工具

    1234567890 1请按照我是想额度插入fvtgb6yhn7ujm8ik,9ol.

  9. Selenium4+Python3系列(十一) - Page Factory设计模式

    写在前面: Page Object模式,目的是将元素定位和元素操作分层,只接触测试内容,不写基础内容,便于后续对自动化测试用例体系的维护,这是中心思想,也是核心. 那么我们继续将简洁延续,这里沿用Ja ...

  10. mysql数据库(字段约束条件)

    什么是字段约束 字段约束就是将字段的内容定一个规则,我们要按照规则办事 约束 描述 关键字 非空约束 限制该字段的数据不能为null not null 唯一约束 保证该字段的所有数据都是唯一.不重复的 ...