- 开发无止境 -
Data: 2022-01-26 12:14:32Form: JournalClick: 11
Golang的atomic
包提供了一组原子操作函数,用于在多个goroutine
之间安全地访问和修改共享变量。这些原子操作函数可以保证对共享变量的操作原子性
的,从而避免了竞态条件的发生。本文将深入探讨Golang的atomic包的原子操作。
Golang的atomic包的原子操作是通过CPU指令
实现的。在大多数CPU架构中,原子操作的实现都是基于32位
或64位
的寄存器。Golang的atomic包的原子操作函数会将变量的地址转换为指针型的变量,并使用CPU指令对这个指针型的变量进行操作。
例如,当我们调用AddInt32函数时,Golang会将变量的地址转换为int32类型的指针,并使用CPU提供的原子指令
对这个指针型的变量进行加法操作。这样,就可以保证对共享变量的操作是原子性的。
需要注意的是,不同的CPU架构可能会提供不同的原子指令。因此,在使用atomic包的原子操作时,需要根据具体的CPU架构来选择合适的原子操作函数。
在x86架构的CPU上,原子操作是通过lock指令
实现的。lock指令
可以将内存操作变成原子操作,保证多个CPU同时访问同一内存地址时的正确性。例如,下面是一个在x86架构上实现的AddInt32函数的汇编代码:
在上面的代码中,我们定义了一个AddInt32函数,它接受三个参数:ptr
、old
和new
。这些参数分别表示要操作的内存地址、旧值和新值。在函数的实现中,我们使用了lock指令
将XADDL
指令变成了原子操作,保证了多个CPU同时访问同一内存地址时的正确性。
Golang的atomic包提供了一组原子操作函数,包括Add
、CompareAndSwap
、Load
、Store
、Swap
等函数。这些函数的具体作用如下:
让我们更具体地来看一下Golang的atomic包的原子操作:
Add函数用于对一个整数型的变量进行加法操作,并返回新的值。Add函数的定义如下:
其中,addr表示要进行加法操作的变量的地址,delta表示要加上的值。Add函数会将变量的值加上delta,并返回新的值。
CompareAndSwap函数用于比较并交换一个指针型的变量的值。如果变量的值等于旧值,就将变量的值设置为新值,并返回true;否则,不修改变量的值,并返回false。CompareAndSwap函数的定义如下:
其中,addr表示要进行比较和交换的变量的地址,old表示旧值,new表示新值。如果变量的值等于旧值,就将变量的值设置为新值,并返回true;否则,不修改变量的值,并返回false。
Load函数用于获取一个指针型的变量的值。Load函数的定义如下:
其中,addr表示要获取的变量的地址。Load函数会返回变量的值。
Store函数用于设置一个指针型的变量的值。Store函数的定义如下:
其中,addr表示要设置的变量的地址,val表示要设置的值。Store函数会将变量的值设置为val。
Swap函数用于交换一个指针型的变量的值,并返回旧值。Swap函数的定义如下:
其中,addr表示要交换的变量的地址,new表示新值。Swap函数会将变量的值设置为new,并返回旧值
Add函数用于对一个整数型的变量进行加法操作,并返回新的值。下面是一个Add函数的示例代码:
在上面的代码中,我们定义了一个int32类型的变量count,并将其初始化为0。然后,我们启动了100个goroutine,每个goroutine都会对count变量进行加1操作。在主goroutine中,我们使用atomic.LoadInt32函数来获取count变量的值,如果count变量的值小于100,就继续等待。当count变量的值等于100时,我们打印count变量的值。
CompareAndSwap函数用于比较并交换一个指针型的变量的值。如果变量的值等于旧值,就将变量的值设置为新值,并返回true;否则,不修改变量的值,并返回false。下面是一个CompareAndSwap函数的示例代码:
在上面的代码中,我们定义了一个int32类型的变量count,并将其初始化为0。然后,我们启动了100个goroutine,每个goroutine都会对count变量进行加1操作。在每个goroutine中,我们使用for循环来进行比较和交换操作,直到成功为止
。在主goroutine中,我们使用atomic.LoadInt32函数来获取count变量的值,如果count变量的值小于100,就继续等待。当count变量的值等于100时,我们打印count变量的值。
Load函数用于获取一个指针型的变量的值。下面是一个Load函数的示例代码:
在上面的代码中,我们定义了一个int32类型的变量count,并将其初始化为0。然后,我们启动了一个goroutine,用于不断地获取count变量的值。在主goroutine中,我们使用atomic.AddInt32函数对count变量进行加1操作。由于Load函数是非阻塞的,因此我们可以在另一个goroutine中使用Load函数来获取count变量的值。
Store函数用于设置一个指针型的变量的值。下面是一个Store函数的示例代码:
在上面的代码中,我们定义了一个int32类型的变量count,并将其初始化为0。然后,我们使用atomic.StoreInt32函数将count变量的值设置为100。最后,我们打印count变量的值。
Swap函数用于交换一个指针型的变量的值,并返回旧值。下面是一个Swap函数的示例代码:
在上面的代码中,我们定义了一个int32类型的变量count,并将其初始化为0。然后,我们使用atomic.SwapInt32函数将count变量的值设置为100,并将旧值保存到old变量中。最后,我们打印旧值和新值。
需要注意的是,使用atomic包的原子操作时,需要保证对共享变量的操作都是原子性的。如果在原子操作之外对共享变量进行了操作,就可能会导致竞态条件的发生。因此,在使用atomic包的原子操作时,需要仔细考虑代码的逻辑和数据的共享方式。
总之,在使用atomic包时,需要仔细考虑代码的逻辑和数据的共享方式,避免出现竞态条件和内存模型的问题。同时,需要注意原子操作的性能和数据的对齐方式,以提高程序的效率和正确性。
AddInt32
或者AddInt64
,而没有AddInt16
和AddInt128
呢?在大多数平台上,CPU并不支持原子操作16位整数。在这些平台上,对16位整数进行原子操作需要使用32位整数或64位整数来实现。因此,在Go语言的atomic包中,没有提供AddInt16函数
在早期的CPU架构中,16位整数并不是主流的数据类型,因此CPU并没有专门为16位整数提供原子操作的支持。相反,CPU更加关注32位整数和64位整数的原子操作,因为这些数据类型更加常见和重要。
此外,原子操作需要保证多个CPU同时访问同一内存地址时的正确性,因此需要使用锁机制来实现。锁机制会增加CPU的开销和复杂度,因此CPU需要权衡性能和功能的考虑。在这种情况下,CPU更倾向于支持更常见和重要的数据类型,而不是支持所有可能的数据类型。
使用原子操作可以保证多个线程或进程同时访问同一内存地址时的正确性,但并不能保证业务逻辑的正确性。因此,在使用原子操作时,需要注意以下几点,以确保业务逻辑的正确性:
对原子操作返回的结果进行判断处理,至少需要有失败重试机制。
原子操作,只能保证操作的原子性,但是不能保证操作一定成功。