當(dāng)你在Stack Overflow網(wǎng)站標(biāo)題中看到“隨機(jī)”這個詞你基本可以確定這是相同的基本問題無數(shù)的相似問題。本文帶你探討為什么隨機(jī)性會引起這么多問題并且如何解決它們。
Stack Overflow (or newsgroup, or mailing list etc) )網(wǎng)站的問題通常是這樣的:
我使用Random.Next生成隨機(jī)數(shù),但它一直給我相同的號碼。 它不停的運(yùn)行,但每次它會產(chǎn)生相同數(shù)量很多次。
這是由于這樣的代碼:
// Bad code! Do not use! for (int i = 0; i < 100; i++) { Console.WriteLine(GenerateDigit()); } .... static int GenerateDigit() { Random rng = new Random(); // Assume there'd be more logic here really return rng.Next(10); }那么,這程序到底出了什么問題?
1.解讀
這種Random類不是真正的隨機(jī)數(shù)發(fā)生器,它是一個偽隨機(jī)數(shù)發(fā)生器。任何Random實(shí)例都有一定量的狀態(tài),而當(dāng)你調(diào)用Next( or NextDouble or NextBytes),它會使用該狀態(tài)來返回到似乎是隨機(jī)的數(shù)據(jù),相應(yīng)的改變它內(nèi)部狀態(tài)以便于在下一步調(diào)用時你將得到另一個偽隨機(jī)數(shù)。
所有的這一切都是確定的,如果你開始一個Random的實(shí)例以相同的初始狀態(tài)(可通過種子來提供),并使用相同的序列方法調(diào)用它,那你會得到相同的結(jié)果。
那么在我們的示例代碼中到底出了什么問題? 我們使用的一個新的Random實(shí)例也在循環(huán)迭代。隨機(jī)無參數(shù)的構(gòu)造函數(shù)取當(dāng)前日期和時間作為種子-在內(nèi)部定時器工作之前你通常可以執(zhí)行大量代碼,當(dāng)前的日期和時間就會發(fā)生變化。 因此,我們重復(fù)使用相同的種子就會重復(fù)得到相同的結(jié)果。
2.對此我們能做什么?
這個問題有很多的解決方案, 其中有些方法是比其他的更好。 讓我們先挑出其中一種方法,因?yàn)樗煌谄渌姆椒ā?/p>
3.使用加密的隨機(jī)數(shù)發(fā)生器
.NET有一個RandomNumberGenerator類應(yīng)該是所有加密隨機(jī)數(shù)生成器派生而來的抽象類。 這個框架本身附帶了一個這樣的派生類: RNGCryptoServicePRovider 。 加密隨機(jī)數(shù)發(fā)生器的理念是,即使它可能仍然是一個偽隨機(jī)生成器,它還是很難做到不可預(yù)料。 內(nèi)置的實(shí)現(xiàn)需要多個熵源在你的電腦有效地呈現(xiàn)“噪音”,并難以預(yù)測。它可以使用這種噪音不僅僅是計(jì)算一個種子,也可以在生成下一個數(shù)字時讓你知道當(dāng)前的狀態(tài),這也許可能不足以預(yù)測下一個結(jié)果(或者那些已經(jīng)生成),這主要取決于具體的實(shí)施。Windows也可以利用專業(yè)硬件資源的隨機(jī)性(如一塊硬件觀察放射性同位素衰變),從而使得隨機(jī)數(shù)發(fā)生器更加安全。
相比于這種隨機(jī),如果你看到(說)10個結(jié)果調(diào)用Random.Next(100)并投入大量計(jì)算資源任務(wù),你可能會制定出最初的種子并預(yù)知接下來的結(jié)果將是...很有可能也會知道之前的結(jié)果是什么。 如果這種隨機(jī)數(shù)應(yīng)用于證券或金融的目的,這會是災(zāi)難性的事態(tài)。 加密隨機(jī)數(shù)生成器通常比Random慢 ,但它在賦予數(shù)字難以預(yù)測和獨(dú)立方面做得更好。
在很多情況下,隨機(jī)數(shù)生成器的性能不是一個問題-但有一個適當(dāng)?shù)腁PI就會出現(xiàn)問題。 隨機(jī)數(shù)字生成器設(shè)計(jì)基礎(chǔ)僅此是用來生成隨機(jī)字節(jié)。比較這種API的隨機(jī) ,它可以讓你請求一個隨機(jī)整數(shù),或隨機(jī)double,或一組隨機(jī)字節(jié)。我經(jīng)常發(fā)現(xiàn)我需要一個整數(shù)的范圍,得到可靠且一致地隨機(jī)字節(jié)數(shù)組是很重要的。這不是不可能,但至少你可能會想要一個適配器類在隨機(jī)數(shù)字生成器上。大多情況下,如果你能避免前面所述的陷阱,偽隨機(jī)性的Random是可以接受的。
讓我們看看如何能做到這一點(diǎn)。
4.用一個復(fù)用的實(shí)例Random
對于“大量重復(fù)的數(shù)字”的修復(fù)程序的核心是重復(fù)使用同一個實(shí)例Random。 這聽起來很簡單...例如,我們可以改變我們這樣原始的代碼像這樣:
// Somewhat better code... Random rng = new Random();for (int i = 0; i < 100; i++) { Console.WriteLine(GenerateDigit(rng)); } ...static int GenerateDigit(Random rng) { // Assume there'd be more logic here really return rng.Next(10); }現(xiàn)在,我們的循環(huán)會打印不同的數(shù)字......但我們還沒有完成。假如你在快速連續(xù)的時間內(nèi)調(diào)用此代碼會發(fā)生什么? 我們可能仍然需要創(chuàng)建的兩個Random實(shí)例使用相同的種子......雖然數(shù)字的每個字符串將包含不同的數(shù)字,我們可以很容易得到的數(shù)字相同的字符串的兩倍。
有兩種方式可以避免這個問題。 一種方式是使用一個靜態(tài)字段保持的單個實(shí)例Random被每一個對象使用。另外,我們可以推高實(shí)例,當(dāng)然是最終達(dá)到計(jì)劃時,這永遠(yuǎn)只能實(shí)例化一個單一的元素隨機(jī)性 ,并將其傳遞到任意地方。這是一個不錯的主意(和它所表達(dá)的依賴性很好),但它不會完全的工作......至少,如果你的代碼使用多個線程它會引發(fā)問題。
5.線程安全
Random不是線程安全的。這是一個真正的痛處,因?yàn)榭紤]到我們觀念上是想在任何程序中如何使用單個實(shí)例。 但事實(shí)是,如果你從多個線程使用相同實(shí)例,它很可能以全零內(nèi)部狀態(tài)結(jié)束,此時該實(shí)例變得無用。
再次,在這里有兩種方法可以解決這個問題。其一是仍然使用一個實(shí)例, 而且使用的每個調(diào)用方必須記住他們所使用的隨機(jī)數(shù)生成器,同時獲得鎖。通過使用一個包裝器鎖定你就可以達(dá)到簡化的效果,但在一個高度多線程系統(tǒng)中你仍然有可能浪費(fèi)大量的時間等待加鎖。
在這里我們將學(xué)會另一種方法 - 是讓每個線程有一個實(shí)例。 我們需要確保,當(dāng)我們創(chuàng)建實(shí)例時我們不要重復(fù)使用相同的種子(例如,所以我們不能只調(diào)用無參數(shù)的構(gòu)造函數(shù)),但除此之外它是相對簡單的。
6.一個安全驅(qū)動
很幸運(yùn)的是,新ThreadLocal<T> .NET4類使得它很容易在每個線程需要單個實(shí)例中編寫提供者。 您只需給ThreadLocal<T>構(gòu)造一個委托調(diào)用來獲得初始值當(dāng)你不在的時候。 就我而言,我選擇使用一個單一的種子變量,初始化使用Environment.TickCount(就像參數(shù)的Random構(gòu)造函數(shù)),然后每遞增,我們需要一個新的隨機(jī)數(shù)生成器的時間-這是每一次的線程。
整個類是靜態(tài)的,只有一種公開方法: 隨機(jī)獲得線程 。這是一個方法而不是一個屬性大多為方便起見:而不是讓其中需要隨機(jī)數(shù)的類依賴于Random本身,他們會依賴于Func<Random> 。 如果這類型僅設(shè)計(jì)在單個線程中運(yùn)行,它可以調(diào)用委托獲得的單個實(shí)例Random和重復(fù)使用; 假如它能夠從多個線程中每次使用調(diào)用委托它就需要一個隨機(jī)數(shù)發(fā)生器。 這將只會創(chuàng)造盡可能多的實(shí)例有線程,每個將使用不同的種子開始。 在依賴傳球的時候,我們就可以用一個方法轉(zhuǎn)換:
new TypeThatNeedsRandom(RandomProvider.GetThreadRandom) 下面的代碼:
using System;using System.Threading; public static class RandomProvider { private static int seed = Environment.TickCount; private static ThreadLocal<Random> randomWrapper = new ThreadLocal<Random>(() => new Random(Interlocked.Increment(ref seed)) ); public static Random GetThreadRandom() { return randomWrapper.Value; } }很簡單,不是嗎? 這是因?yàn)樗乃嘘P(guān)注的是提供正確的Random實(shí)例 。 它并不在乎你采用什么樣方法調(diào)用已經(jīng)獲取的實(shí)例。 代碼仍然可以濫用這個類,當(dāng)然,通過存放一個隨機(jī)引用并用多個線程重復(fù)使用它,但要做對的事還是很容易的。
7.界面設(shè)計(jì)問題
一個問題仍然存在:這依舊不是很安全的。 正如我前面提到的,最常用的派生類是RNGCryptoServiceProvider,還有一個更安全隨機(jī)數(shù)字發(fā)生器的版本,然而這個API在一般情況下還是很難使用。
假如框架驅(qū)動已經(jīng)從“我想以簡單的方法得到一個隨機(jī)值”的概念中分離概念的“隨機(jī)性源”,這確實(shí)是令人非常愉快的。 然后我們可以根據(jù)需要使用一個簡單的API來支持一個安全的或不安全的隨機(jī)源,很不幸的是,還沒有這樣的方法。也許在將來的迭代中......或者有個第三方會想出一個適配器來代替。(可惜這在我能力之上,很好地做好這件事情是相當(dāng)困難的。)你幾乎可以輕松成功地派生隨機(jī)和覆蓋示例及下個字節(jié) ......但目前還不清楚他們需要如何工作,甚至Sample可能會非常棘手。 也許下一次...
這是一篇國外的文章,被我翻譯過來。原文地址:http://csharpindepth.com/Articles/Chapter12/Random.aspx
接受批評指正,拒絕無腦噴糞。
新聞熱點(diǎn)
疑難解答
圖片精選