golang 利用原子操作实现唯一ID生成器

前言

在作为练习golang建立的IM项目中有个需求,客户端连接上来并且通过认证之后需要为客户端连接生成一个唯一的id,用于在业务中快速定位这条连接。有个可选的做法是将用户uid作为这个标识。但是出于减少模块耦合还有自身强迫症最终使用生成唯一ID的做法。这个唯一id要求在并发环境下保持不重复。 我能想到的第一种做法是加锁实现,但是在golang似乎有更简洁更高效的做法。用原子操作实现。

依赖

代码依赖标准库atomic:import "sync/atomic"

代码

废话先不多说,直接上个代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//GetIncreaseID 并发环境下生成一个增长的id,按需设置局部变量或者全局变量
func GetIncreaseID(ID *uint64) uint64 {
	var n, v uint64
	for {
		v = atomic.LoadUint64(ID)
		n = v + 1
		if atomic.CompareAndSwapUint64(ID, v, n) {
			break
		}
	}
	return n
}

原子操作

第一次接触到原子操作这个概念,我是懵逼的,作为计算机专业出身的我居然对这个词汇没有任何印象,一定是当时没有好好学,这是定论。又废话了…… 好,原子操作是受CPU硬件级支持的操作,比基于操作系统API的锁要高效。在针对某个值的原子操作正在进行的过程中CPU不会进行其他针对该值的操作,原子操作仅由一个独立的CPU指令来完成。保证在并发环境下原子操作的绝对安全。

golang标准库提供了对int32、int64、uint32、uint64、uintptr和unsafe.Pointer6个数据类型的增或减、比较并交换、载入、存储和交换5种操作。具体的这里不再一一赘述。只说明函数中用到的。

函数分析

本函数接受一个uint64的指针并返回一个uint64类型的数值。函数体中主体部分:在一个for死循环中,先用func LoadUint64(addr *uint64) (val uint64)函数将指针参数ID指向的值“安全的”加载进来。为什么不是用v=*ID直接取值呢?

LoadUint64函数

由于这种方式在大并发环境下同一时间内其他进程/线程对*ID这个值的读写操作是不被限制的,并不安全。在32位架构的系统中甚至会出现只读到一半的情况(数值被写入一半的时候执行了读操作)。用“载入”系列函数(以load开头)进行读原子操作可以避免这种状况。以上即用v = atomic.LoadUint64(ID)而不用v=*ID取值的原因。

CompareAndSwapUint64函数

取值之后对值进行+1操作,然后当atomic.CompareAndSwapUint64(ID, v, n)结果成立的时候退出循环,最终返回函数结果成立时的v+1的值,即我们需要的不重复的递增值。那么func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)函数有何作用就成了关键了,通俗的讲本函数有比较并交换的功用:首先拿 addr指针所指向的值与old值进行比较,如果这两个值相等也就是指针指向的值未被改变那么用new值来替换指针地址所指向的值。替换成功之后返回true,此时的new值也就是我们最终需要生成的唯一递增的值。ok,目的达成,退出循环返回生成的值。

说的有点绕,有任何问题欢迎在留言区指正。

updatedupdated2020-05-072020-05-07
加载评论