二进制与 Go 的原子操作
前置浏览:
- C 语言中文网 - 汇编语言基本概念简介 - 补码及进制转换
- 《GO 并发编程实战》—— 原子操作
二进制相干根底概念
有符号二进制整数有负数和正数。在 x86 处理器中,MSB 示意的是符号位:0 示意负数,1 示意正数。下图展现了 8 位的负数和正数:
概念总结:
- 反码、补码是二进制的一种表现形式;
- 在计算机内所有数值底层都用补码示意,无论正负数(十进制);
- 如果一串二进制值须要视为数值则须要将其视为补码;
- 反码是十进制转二进制计算的一个过程即对一个十进制取补码的过程,个别用在正数转换规则上;
- 反码能够通过二进制值按位取反失去(所有二进制位都取反);
- 负数(十进制)的补码是其二进制自身,正数(十进制)的补码是十进制正数的绝对值求补码后取反码加一;
-
示意负数的补码能够间接转成十进制,示意正数的补码想要转回十进制步骤如下:
- 对示意正数的补码取反码加一失去正数的十进制绝对值补码;
- 再将正数的十进制绝对值补码转成十进制失去正数的十进制绝对值;
- 最初加上符号位;
- 无论是负数加负数(十进制加法)还是负数 / 正数加正数(十进制减法)都能够用补码加补码示意;
- 一个值的负数的补码与其正数的补码相加等于 0;
反码
反码能够通过二进制值按位取反失去(所有二进制位都取反)
负数的反码示例:
十进制数值 | 补码 | 反码 |
---|---|---|
0 | 0000 0000 | 1111 1111 |
1 | 0000 0001 | 1111 1110 |
2 | 0000 0010 | 1111 1101 |
3 | 0000 0011 | 1111 1100 |
4 | 0000 0100 | 1111 1011 |
正数的反码示例:
十进制数值 | 补码 | 反码 |
---|---|---|
-0 | 0000 0000 | 1111 1111 |
-1 | 1111 1111 | 0000 0000 |
-2 | 1111 1110 | 0000 0001 |
-3 | 1111 1101 | 0000 0010 |
补码(十进制转二进制)
在计算机内所有数值底层都用补码示意,无论正负数(十进制)
十进制数值 | 补码 |
---|---|
0 | 0000 0000 |
1 | 0000 0001 |
2 | 0000 0010 |
3 | 0000 0011 |
-0 | 0000 0000 |
-1 | 1111 1111 |
-2 | 1111 1110 |
-3 | 1111 1101 |
正数补码计算过程示例:
十进制数值 | 绝对值 | 绝对值补码 | 绝对值补码取反 | 绝对值补码取反加一 | 正确补码 | 十进制数值 |
---|---|---|---|---|---|---|
-0 | 0 | 0000 0000 | 1111 1111 | 1111 1111 + 1 ————— 1,0000 0000 |
0000 0000 | -0 |
-1 | 1 | 0000 0001 | 1111 1110 | 1111 1110 + 1 ————— 1111 1111 |
1111 1111 | -1 |
-2 | 2 | 0000 0010 | 1111 1101 | 1111 1101 + 1 ————— 1111 1110 |
1111 1110 | -2 |
-3 | 3 | 0000 0011 | 1111 1100 | 1111 1100 + 1 ————— 1111 1101 |
1111 1101 | -3 |
-4 | 4 | 0000 0100 | 1111 1011 | 1111 1011 + 1 ————— 1111 1100 |
1111 1100 | -4 |
-5 | 5 | 0000 0101 | 1111 1010 | 1111 1010 + 1 ————— 1111 1011 |
1111 1011 | -5 |
补码(二进制转十进制)
示意负数的补码能够间接转成十进制,示意正数的补码想要转回十进制步骤如下:
- 对示意正数的补码取反码加一失去正数的十进制绝对值补码;
- 再将正数的十进制绝对值补码转成十进制失去正数的十进制绝对值;
- 最初加上符号位;
MSB | 补码 | 十进制数值 |
---|---|---|
0 | 0000 0000 | 0 |
0 | 0000 0001 | 1 |
0 | 0000 0010 | 2 |
0 | 0000 0011 | 3 |
0 | 0000 0100 | 4 |
0 | 0000 0101 | 5 |
1 | 1111 1111 | -1 |
1 | 1111 1110 | -2 |
1 | 1111 1101 | -3 |
1 | 1111 1100 | -4 |
1 | 1111 1011 | -5 |
正数转换示例:
MSB | 补码 | 补码取反 | 补码取反加一 | 补码取反加一后所代表十进制值 | 符号 | 十进制后果 | 补码 |
---|---|---|---|---|---|---|---|
1 | 1111 1111 | 0000 0000 | 0000 0001 | 1 | – | -1 | 1111 1111 |
1 | 1111 1110 | 0000 0001 | 0000 0010 | 2 | – | -2 | 1111 1110 |
1 | 1111 1101 | 0000 0010 | 0000 0011 | 3 | – | -3 | 1111 1101 |
1 | 1111 1100 | 0000 0011 | 0000 0100 | 4 | – | -4 | 1111 1100 |
1 | 1111 1011 | 0000 0100 | 0000 0101 | 5 | – | -5 | 1111 1011 |
补码相加
无论是负数加负数(十进制加法)还是负数 / 正数加正数(十进制减法)都能够用补码加补码示意
负数加负数的补码计算过程示例:
表达式 | 补码相加 | 二进制后果 | 十进制后果 |
---|---|---|---|
0+0 | 0000 0000 + 0000 0000 —————— 0000 0000 |
0000 0000 | 0 |
0+1 | 0000 0000 + 0000 0001 —————— 0000 0001 |
0000 0001 | 1 |
1+1 | 0000 0001 + 0000 0001 —————— 0000 0010 |
0000 0010 | 2 |
2+1 | 0000 0010 + 0000 0001 —————— 0000 0011 |
0000 0011 | 3 |
负数加正数的补码计算过程示例:
表达式 | 补码相加 | 二进制后果 | 十进制后果 |
---|---|---|---|
0+(-0) | 0000 0000 + 0000 0000 —————— 0000 0000 |
0000 0000 | 0 |
0+(-1) | 0000 0000 + 1111 1111 —————— 1111 1111 |
1111 1111 | -1 |
1+(-1) | 0000 0001 + 1111 1111 —————— 1,0000 0000 |
0000 0000 | 0 |
1+(-2) | 0000 0001 + 1111 1110 —————— 1111 1111 |
1111 1111 | -1 |
2+(-2) | 0000 0010 + 1111 1110 —————— 1,0000 0000 |
0000 0000 | 0 |
2+(-1) | 0000 0010 + 1111 1111 —————— 1,0000 0001 |
0000 0001 | 1 |
正数加正数的补码计算过程示例:
表达式 | 补码相加 | 二进制后果 | 十进制后果 |
---|---|---|---|
(-0)+(-0) | 0000 0000 + 0000 0000 —————— 0000 0000 |
0000 0000 | 0 |
(-1)+(-1) | 1111 1111 + 1111 1111 —————— 1,1111 1110 |
1111 1110 | -2 |
(-1)+(-2) | 1111 1111 + 1111 1110 —————— 1,1111,1101 |
1111 1101 | -3 |
二进制、反码、补码
同样的一串二进制数字,即能够是反码也能够是补码,如果是补码则其能够通过上述规定转成对应的十进制数值,如果是反码则代表其为计算过程两头值,如果想晓得反码在十进制中所示意的数值,能够将其视为补码再通过上述规定转成十进制即可。
负数示例:
十进制数值 x | x 取补码 fn1(x)=a | x 取反码 fn2(x)=b | b 的十进制模式 y |
---|---|---|---|
0 | 0000 0000 | 1111 1111 | -1 |
1 | 0000 0001 | 1111 1110 | -2 |
2 | 0000 0010 | 1111 1101 | -3 |
3 | 0000 0011 | 1111 1100 | -4 |
4 | 0000 0100 | 1111 1011 | -5 |
正数示例:
十进制数值 x | x 取补码 fn1(x)=a | x 取反码 fn2(x)=b | b 的十进制模式 y |
---|---|---|---|
-0 | 0000 0000 | 1111 1111 | -1 |
-1 | 1111 1111 | 0000 0000 | 0 |
-2 | 1111 1110 | 0000 0001 | 1 |
-3 | 1111 1101 | 0000 0010 | 2 |
示例汇总:
十进制数值 x | x 取补码 fn1(x)=a | x 取反码 fn2(x)=b | b 的十进制模式 y | y + 1 | 十进制数值 x |
---|---|---|---|---|---|
0 | 0000 0000 | 1111 1111 | -1 | 0 | 0 |
1 | 0000 0001 | 1111 1110 | -2 | -1 | 1 |
2 | 0000 0010 | 1111 1101 | -3 | -2 | 2 |
3 | 0000 0011 | 1111 1100 | -4 | -3 | 3 |
-0 | 0000 0000 | 1111 1111 | -1 | 0 | -0 |
-1 | 1111 1111 | 0000 0000 | 0 | 1 | -1 |
-2 | 1111 1110 | 0000 0001 | 1 | 2 | -2 |
-3 | 1111 1101 | 0000 0010 | 2 | 3 | -3 |
通过该表格示例能够得出以下两个法则:
法则 一
反码所示意的数值与原数值之间法则如下(y 代表反码之后的十进制值):
- fn2(x) = -x-1
- fn2(x) + 1 = -x
- y = -x-1
- y +1 = -x
即如果想得到一个十进制正数值的正数模式(1 => -1)或则失去一个十进制负数值的负数模式能够通过对原值取反码加一失去:
十进制数值 x | 十进制取反 -x | 过程 |
---|---|---|
0 | 0 | 取反码 (0)+1 = -1+1 |
1 | -1 | 取反码 (1)+1 = -2+1 |
2 | -2 | 取反码 (2)+1 = -3+1 |
3 | -3 | 取反码 (3)+1 = -4+1 |
-1 | 1 | 取反码 (-1)+1 = 0+1 |
-2 | 2 | 取反码 (-2)+1 = 1+1 |
-3 | 3 | 取反码 (-3)+1 = 2+1 |
法则 二
将示例汇总表格再进一步简化:
十进制数值 x | x 的反码十进制示意模式 y | 翻译 -1 | 翻译 -2 |
---|---|---|---|
0 | -1 | 0 的反码是 -1 | -1 是 0 的反码 |
1 | -2 | 1 的反码是 -2 | -2 是 1 的反码 |
2 | -3 | 2 的反码是 -3 | -3 是 2 的反码 |
3 | -4 | 3 的反码是 -4 | -4 是 3 的反码 |
-0 | -1 | -0 的反码是 -1 | -1 是 -0 的反码 |
-1 | 0 | -1 的反码是 0 | 0 是 -1 的反码 |
-2 | 1 | -2 的反码是 1 | 1 是 -2 的反码 |
-3 | 2 | -3 的反码是 2 | 2 是 -3 的反码 |
能够看出在十进制格局下,原数值与反码的关系:
- 如果我须要 -1 我能够用 0 的反码代替;
- 如果我须要 -4 我能够用 3 的反码代替;
-
法则:
- x = |y| -1
- x + y = -1
Go 的体现
二进制的输入格局
在 Go 语言中,一个数值是负数或正数,无论是何种打印形式,输入的都会待上正负号:
fmt.Printf("1 的十进制:%v\n",1)
fmt.Printf("-1 的十进制:%v\n",-1)
fmt.Printf("-1 的二进制 ( 简化版):%v\n",strconv.FormatInt(-1,2))
fmt.Printf("1 的二进制:%064b\n",1) // 占 64 位宽,有余补 0
fmt.Printf("-1 的二进制:%064b\n",-1) // 占 64 位宽,有余补 0
fmt.Printf("4 的十进制:%v\n",4)
fmt.Printf("-4 的十进制:%v\n",-4)
fmt.Printf("-4 的二进制 ( 简化版):%v\n",strconv.FormatInt(-4,2))
fmt.Printf("4 的二进制:%064b\n",4) // 占 64 位宽,有余补 0
fmt.Printf("-4 的二进制:%064b\n",-4) // 占 64 位宽,有余补 0
// 输入
// 1 的十进制:1
// -1 的十进制:-1
// -1 的二进制 (简化版):-1
// 1 的二进制:0000000000000000000000000000000000000000000000000000000000000001
// -1 的二进制:-000000000000000000000000000000000000000000000000000000000000001
// 4 的十进制:4
// -4 的十进制:-4
// -4 的二进制 (简化版):-100
// 4 的二进制:0000000000000000000000000000000000000000000000000000000000000100
// -4 的二进制:-000000000000000000000000000000000000000000000000000000000000100
- 能够看出输入二进制时 Go 的输入与十进制一样同样将符号位具象化,而非输入对应的 0 或 1;
- 输入二进制时数值局部则取其绝对值的补码;
如果咱们想要看到正确的正数的补码模式则须要通过无符号数值类型间接实现:
- 无符号数值类型如何示意一个数的正数模式,答案是补码取反码加一;
- 譬如如何用无符号数值类型示意 -1:
^uint8(1) + 1
; - ^ 符号在二元运算中代表亦或符;在一元运算中代表取反码符;
fmt.Printf("int8(1) : %08b \n", int8(1)) // 占 8 位宽,有余补 0
fmt.Printf("^int8(1) : %08b \n", ^int8(1)) // 占 8 位宽,有余补 0
fmt.Printf("^int8(1)+1 : %08b \n", ^int8(1)+1) // 占 8 位宽,有余补 0
fmt.Printf("uint8(1) : %08b \n", uint8(1)) // 占 8 位宽,有余补 0
fmt.Printf("^uint8(1) : %08b \n", ^uint8(1)) // 占 8 位宽,有余补 0
fmt.Printf("^uint8(1)+1 : %08b \n", ^uint8(1)+1)// 占 8 位宽,有余补 0
// 输入
// int8(1) : 00000001
// ^int8(1) : -0000010
// ^int8(1)+1 : -0000001
// uint8(1) : 00000001
// ^uint8(1) : 11111110
// ^uint8(1)+1 : 11111111
- 能够看到通过应用无符号数值类型对 1 取反后失去的是 -2 的补码模式
1111 1110
,接着对反码后加一失去原值 1 的正数模式 -1 的补码1111 1111
;
Go 的原子操作
无论是有符号数值类型还是无符号数值类型,只有转成补码后进行的计算过程不须要思考符号位的问题。
A – B = A + (-B)
A – B = 补码 (A) + 补码 (-B)
原子操作即是进行过程中不能被中断的操作。也就是说,针对某个值的原子操作在被进行的过程当中,CPU 绝不会再去进行其它的针对该值的操作。无论这些其它的操作是否为原子操作都会是这样。为了实现这样的严谨性,原子操作仅会由一个独立的 CPU 指令代表和实现。只有这样才可能在并发环境下保障原子操作的相对平安。
Go 语言提供的原子操作都是非侵入式的。它们由规范库代码包 sync/atomic 中的泛滥函数代表。咱们能够通过调用这些函数对几种简略的类型的值进行原子操作。这些类型包含 int32、int64、uint32、uint64、uintptr 和 unsafe.Pointer 类型,共 6 个。这些函数提供的原子操作共有 5 种,即:增或减、比拟并替换、载入、存储和替换。
原子操作 – 增或减
相干文档如图所示:
这里次要须要留神的是 Uint 类型的原子操作,以 AddUint32 函数为例
原子性的减少数值:
value := uint32(1)
atomic.AddUint32(&value, 1)
fmt.Printf("after call atomic.AddUint32(&value, 1) value is: %v\n", value)
atomic.AddUint32(&value, 2)
fmt.Printf("after call atomic.AddUint32(&value, 2) value is: %v\n", value)
atomic.AddUint32(&value, 3)
fmt.Printf("after call atomic.AddUint32(&value, 3) value is: %v\n", value)
// 输入
// after call atomic.AddUint32(&value, 1) value is: 2
// after call atomic.AddUint32(&value, 2) value is: 4
// after call atomic.AddUint32(&value, 3) value is: 7
原子性的缩小数值:
如文档所述,如果须要减去一个负数 c 须要通过 ^uint32(c-1)
计算失去 c 的补码。
const one, two, three = 1, 2, 3
value := uint32(10)
atomic.AddUint32(&value, ^uint32(one - 1)) // 减一
fmt.Printf("after callatomic.AddUint32(&value, ^uint32(one - 1)) value is: %v\n", value)
atomic.AddUint32(&value, ^uint32(two - 1)) // 减二
fmt.Printf("after callatomic.AddUint32(&value, ^uint32(two - 1)) value is: %v\n", value)
atomic.AddUint32(&value, ^uint32(three - 1)) // 减三
fmt.Printf("after callatomic.AddUint32(&value, ^uint32(three - 1)) value is: %v\n", value)
// 输入
// after callatomic.AddUint32(&value, ^uint32(one - 1)) value is: 9
// after callatomic.AddUint32(&value, ^uint32(two - 1)) value is: 7
// after callatomic.AddUint32(&value, ^uint32(three - 1)) value is: 4
- value -1 等价于 value + (-1),等价于 补码 (value) + 补码 (-1);
- 通过后面二进制法则二 得悉,求 -1 的补码相当于求 0 的反码;
- go 的反码运算符位
^
; - 联合后便可实现了无符号类型数据的减法运算。
参考
- C 语言中文网 - 汇编语言基本概念简介 - 补码及进制转换
- 《GO 并发编程实战》—— 原子操作