Go中使用Seed获得重复随机数的问题

1. 重复的随机数

废话很少说,首先咱们来看使用seed的一个很神奇的现象。java

func main() {
	for i := 0; i < 5; i++ {
    rand.Seed(time.Now().Unix())
		fmt.Println(rand.Intn(100))
	}
}

// 结果以下
// 90
// 90
// 90
// 90
// 90
复制代码

可能不熟悉seed用法的看到这里会很疑惑,我不是都用了seed吗?为什么我随机出来的数字都是同样的?不该该每次都不同吗?web

可能会有人说是你数据的样本空间过小了,OK,咱们加大样本空间到10w再试试。服务器

func main() {
	for i := 0; i < 5; i++ {
    rand.Seed(time.Now().Unix())
		fmt.Println(rand.Intn(100000))
	}
}

// 结果以下
// 84077
// 84077
// 84077
// 84077
// 84077
复制代码

你会发现结果仍然是同样的。简单的推理一下咱们就能知道,在上面那种状况,每次都取到相同的随机数跟咱们所取的样本空间大小是无关的。那么惟一有关的就是seed。咱们首先得明确seed的用途。微信

2. seed的用途

在这里就不卖关子了,先给出结论。并发

上面每次获得相同随机数是由于在上面的循环中,每次操做的间隔都在毫秒级下,因此每次经过time.Now().Unix()取出来的时间戳都是同一个值,换句话说就是使用了同一个seed。app

这个其实很好验证。只须要在每次循环的时候将生成的时间戳打印出来,你就会发现每次打印出来的时间戳都是同样的。frontend

每次rand都会使用相同的seed来生成随机队列,这样一来在循环中使用相同seed获得的随机队列都是相同的,而生成随机数时每次都会去取同一个位置的数,因此每次取到的随机数都是相同的。dom

seed 只用于决定一个肯定的随机序列。无论seed多大多小,只要随机序列一肯定,自己就不会再重复。除非是样本空间过小。解决方案有两种:函数

  • 在全局初始化调用一次seed便可
  • 每次使用纳秒级别的种子(强烈不推荐这种)

3. 不用每次调用

上面的解决方案建议各位不要使用第二种,给出是由于在某种状况下的确能够解决问题。好比在你的服务中使用这个seed的地方是串行的,那么每次获得的随机序列的确会不同。微服务

可是若是在高并发下呢?你可以保证每次取到的仍是不同的吗?事实证实,在高并发下,即便使用UnixNano做为解决方案,一样会获得相同的时间戳,Go官方也不建议在服务中同时调用。

Seed should not be called concurrently with any other Rand method.

接下来会带你们了解一下代码的细节。想了解源码的能够继续读下去。

4. 源码解析-seed

4.1 seed

首先来看一下seed作了什么。

func (rng *rngSource) Seed(seed int64) {
	rng.tap = 0
	rng.feed = rngLen - rngTap

	seed = seed % int32max
	if seed < 0 {  // 若是是负数,则强行转换为一个int32的整数
		seed += int32max
	}
	if seed == 0 { // 若是seed没有被赋值,则默认给一个值
		seed = 89482311
	}

	x := int32(seed)
	for i := -20; i < rngLen; i++ {
		x = seedrand(x)
		if i >= 0 {
			var u int64
			u = int64(x) << 40
			x = seedrand(x)
			u ^= int64(x) << 20
			x = seedrand(x)
			u ^= int64(x)
			u ^= rngCooked[i]
			rng.vec[i] = u
		}
	}
}
复制代码

首先,seed赋值了两个定义好的变量,rng.taprng.feedrngLenrngTap是两个常量。咱们来看一下相关的常量定义。

const (
	rngLen   = 607
	rngTap   = 273
	rngMax   = 1 << 63
	rngMask  = rngMax - 1
	int32max = (1 << 31) - 1
)
复制代码

因而可知,不管seed是否相同,这两个变量的值都不会受seed的影响。同时,seed的值会最终决定x的值,只要seed相同,则获得的x就相同。并且不管seed是否被赋值,只要检测到是零值,都会默认的赋值为89482311

接下来咱们再看seedrand。

4.2 seedrand

// seed rng x[n+1] = 48271 * x[n] mod (2**31 - 1)
func seedrand(x int32) int32 {
	const (
		A = 48271
		Q = 44488
		R = 3399
	)

	hi := x / Q 	  // 取除数
	lo := x % Q 	  // 取余数
	x = A*lo - R*hi // 经过公式从新给x赋值
	if x < 0 {
		x += int32max // 若是x是负数,则强行转换为一个int32的正整数
	}
	return x
}
复制代码

能够看出,只要传入的x相同,则最后输出的x必定相同。进而最后获得的随机序列rng.vec就相同。

到此咱们验证咱们最开始给出的结论,即只要每次传入的seed相同,则生成的随机序列就相同。验证了这个以后咱们再继续验证为何每次取到的随机序列的值都是相同的。

5. 源码解析-Intn

首先举个例子,来直观的描述上面提到的问题。

func printRandom() {
  for i := 0; i < 2; i++ {
    fmt.Println(rand.Intn(100))
  }
}

// 结果
// 81
// 87
// 81
// 87
复制代码

假设printRandom是一个单独的Go文件,那么你不管run多少次,每次打印出来的随机序列都是同样的。经过阅读seed的源码咱们知道,这是由于生成了相同的随机序列。那么为何会每次都取到一样的值呢?不说废话,咱们一层一层来看。

5.1 Intn

func (r *Rand) Intn(n int) int {
	if n <= 0 {
		panic("invalid argument to Intn")
	}
	if n <= 1<<31-1 {
		return int(r.Int31n(int32(n)))
	}
	return int(r.Int63n(int64(n)))
}
复制代码

能够看到,若是n小于等于0,就会直接panic。其次,会根据传入的数据类型,返回对应的类型。

虽说这里调用分红了Int31n和Int63n,可是往下看的你会发现,其实都是调用的r.Int63(),只不过在返回64位的时候作了一个右移的操做。

// r.Int31n的调用
func (r *Rand) Int31() int32 { return int32(r.Int63() >> 32) }

// r.Int63n的调用
func (r *Rand) Int63() int64 { return r.src.Int63() }
复制代码

5.2 Int63

先给出这个函数的相关代码。

// 返回一个非负的int64伪随机数.
func (rng *rngSource) Int63() int64 {
	return int64(rng.Uint64() & rngMask)
}

func (rng *rngSource) Uint64() uint64 {
	rng.tap--
	if rng.tap < 0 {
		rng.tap += rngLen
	}

	rng.feed--
	if rng.feed < 0 {
		rng.feed += rngLen
	}

	x := rng.vec[rng.feed] + rng.vec[rng.tap]
	rng.vec[rng.feed] = x
	return uint64(x)
}
复制代码

能够看到,不管是int31仍是int63,最终都会进入Uint64这个函数中。而在这两个函数中,这两个变量的值显得尤其关键。由于直接决定了最后获得的随机数,这两个变量的赋值以下。

rng.tap = 0
rng.feed = rngLen - rngTap
复制代码

tap的值是常量0,而feed的值决定于rngLen和rngTap,而这两个变量的值也是一个常量。如此,每次从随机队列中取到的值都是肯定的两个值的和。

到这,咱们也验证了只要传入的seed相同,而且每次都调用seed方法,那么每次随机出来的值必定是相同的

6. 结论

首先评估是否须要使用seed,其次,使用seed只须要在全局调用一次便可,若是屡次调用则有可能取到相同随机数。

往期文章:

相关:

  • 微信公众号: SH的全栈笔记(或直接在添加公众号界面搜索微信号LunhaoHu)
相关文章
相关标签/搜索