您好,登錄后才能下訂單哦!
這期內容當中小編將會給大家帶來有關Go狼中seed得到相同隨機數如何解決,文章內容豐富且以專業的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
1. 重復的隨機數
廢話不多說,首先我們來看使用seed的一個很神奇的現象。
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嗎?為何我隨機出來的數字都是一樣的?不應該每次都不一樣嗎?
可能會有人說是你數據的樣本空間太小了,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。
這個其實很好驗證。只需要在每次循環的時候將生成的時間戳打印出來,你就會發現每次打印出來的時間戳都是一樣的。
每次rand都會使用相同的seed來生成隨機隊列,這樣一來在循環中使用相同seed得到的隨機隊列都是相同的,而生成隨機數時每次都會去取同一個位置的數,所以每次取到的隨機數都是相同的。
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.tap和rng.feed。rngLen和rngTap是兩個常量。我們來看一下相關的常量定義。
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方法,那么每次隨機出來的值一定是相同的。
上述就是小編為大家分享的Go狼中seed得到相同隨機數如何解決了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。