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)
相關文章
相關標籤/搜索