當你在Stack Overflow網站標題中看到“隨機”這個詞你基本可以確定這是相同的基本問題無數的相似問題。本文帶你探討為什麼隨機性會引起這麼多問題並且如何解決它們。
Stack Overflow (or newsgroup, or mailing list etc) )網站的問題通常是這樣的:
我使用Random.Next生成隨機數,但它一直給我相同的號碼。 它不停的運行,但每次它會產生相同數量很多次。
這是由於這樣的代碼:
// 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類不是真正的隨機數發生器,它是一個偽隨機數發生器。任何Random實例都有一定量的狀態,而當你調用Next( or NextDouble or NextBytes),它會使用該狀態來返回到似乎是隨機的數據,相應的改變它內部狀態以便於在下一步調用時你將得到另一個偽隨機數。
所有的這一切都是確定的,如果你開始一個Random的實例以相同的初始狀態(可通過種子來提供),並使用相同的序列方法調用它,那你會得到相同的結果。
那麼在我們的示例代碼中到底出了什麼問題? 我們使用的一個新的Random實例也在循環迭代。隨機無參數的構造函數取當前日期和時間作為種子-在內部定時器工作之前你通常可以執行大量代碼,當前的日期和時間就會發生變化。 因此,我們重復使用相同的種子就會重復得到相同的結果。
2.對此我們能做什麼?
這個問題有很多的解決方案, 其中有些方法是比其他的更好。 讓我們先挑出其中一種方法,因為它不同於其他的方法。
3.使用加密的隨機數發生器
.NET有一個RandomNumberGenerator類應該是所有加密隨機數生成器派生而來的抽象類。 這個框架本身附帶了一個這樣的派生類: RNGCryptoServiceProvider 。 加密隨機數發生器的理念是,即使它可能仍然是一個偽隨機生成器,它還是很難做到不可預料。 內置的實現需要多個熵源在你的電腦有效地呈現“噪音”,並難以預測。它可以使用這種噪音不僅僅是計算一個種子,也可以在生成下一個數字時讓你知道當前的狀態,這也許可能不足以預測下一個結果(或者那些已經生成),這主要取決於具體的實施。Windows也可以利用專業硬件資源的隨機性(如一塊硬件觀察放射性同位素衰變),從而使得隨機數發生器更加安全。
相比於這種隨機,如果你看到(說)10個結果調用Random.Next(100)並投入大量計算資源任務,你可能會制定出最初的種子並預知接下來的結果將是...很有可能也會知道之前的結果是什麼。 如果這種隨機數應用於證券或金融的目的,這會是災難性的事態。 加密隨機數生成器通常比Random慢 ,但它在賦予數字難以預測和獨立方面做得更好。
在很多情況下,隨機數生成器的性能不是一個問題-但有一個適當的API就會出現問題。 隨機數字生成器設計基礎僅此是用來生成隨機字節。比較這種API的隨機 ,它可以讓你請求一個隨機整數,或隨機double,或一組隨機字節。我經常發現我需要一個整數的范圍,得到可靠且一致地隨機字節數組是很重要的。這不是不可能,但至少你可能會想要一個適配器類在隨機數字生成器上。大多情況下,如果你能避免前面所述的陷阱,偽隨機性的Random是可以接受的。
讓我們看看如何能做到這一點。
4.用一個復用的實例Random
對於“大量重復的數字”的修復程序的核心是重復使用同一個實例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); }
現在,我們的循環會打印不同的數字......但我們還沒有完成。假如你在快速連續的時間內調用此代碼會發生什麼? 我們可能仍然需要創建的兩個Random實例使用相同的種子......雖然數字的每個字符串將包含不同的數字,我們可以很容易得到的數字相同的字符串的兩倍。
有兩種方式可以避免這個問題。 一種方式是使用一個靜態字段保持的單個實例Random被每一個對象使用。另外,我們可以推高實例,當然是最終達到計劃時,這永遠只能實例化一個單一的元素隨機性 ,並將其傳遞到任意地方。這是一個不錯的主意(和它所表達的依賴性很好),但它不會完全的工作......至少,如果你的代碼使用多個線程它會引發問題。
5.線程安全
Random不是線程安全的。這是一個真正的痛處,因為考慮到我們觀念上是想在任何程序中如何使用單個實例。 但事實是,如果你從多個線程使用相同實例,它很可能以全零內部狀態結束,此時該實例變得無用。
再次,在這裡有兩種方法可以解決這個問題。其一是仍然使用一個實例, 而且使用的每個調用方必須記住他們所使用的隨機數生成器,同時獲得鎖。通過使用一個包裝器鎖定你就可以達到簡化的效果,但在一個高度多線程系統中你仍然有可能浪費大量的時間等待加鎖。
在這裡我們將學會另一種方法 - 是讓每個線程有一個實例。 我們需要確保,當我們創建實例時我們不要重復使用相同的種子(例如,所以我們不能只調用無參數的構造函數),但除此之外它是相對簡單的。
6.一個安全驅動
很幸運的是,新ThreadLocal<T> .NET4類使得它很容易在每個線程需要單個實例中編寫提供者。 您只需給ThreadLocal<T>構造一個委托調用來獲得初始值當你不在的時候。 就我而言,我選擇使用一個單一的種子變量,初始化使用Environment.TickCount(就像參數的Random構造函數),然後每遞增,我們需要一個新的隨機數生成器的時間-這是每一次的線程。
整個類是靜態的,只有一種公開方法: 隨機獲得線程 。這是一個方法而不是一個屬性大多為方便起見:而不是讓其中需要隨機數的類依賴於Random本身,他們會依賴於Func<Random> 。 如果這類型僅設計在單個線程中運行,它可以調用委托獲得的單個實例Random和重復使用; 假如它能夠從多個線程中每次使用調用委托它就需要一個隨機數發生器。 這將只會創造盡可能多的實例有線程,每個將使用不同的種子開始。 在依賴傳球的時候,我們就可以用一個方法轉換:
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; } }
很簡單,不是嗎? 這是因為它的所有關注的是提供正確的Random實例 。 它並不在乎你采用什麼樣方法調用已經獲取的實例。 代碼仍然可以濫用這個類,當然,通過存放一個隨機引用並用多個線程重復使用它,但要做對的事還是很容易的。
7.界面設計問題
一個問題仍然存在:這依舊不是很安全的。 正如我前面提到的,最常用的派生類是RNGCryptoServiceProvider,還有一個更安全隨機數字發生器的版本,然而這個API在一般情況下還是很難使用。
假如框架驅動已經從“我想以簡單的方法得到一個隨機值”的概念中分離概念的“隨機性源”,這確實是令人非常愉快的。 然後我們可以根據需要使用一個簡單的API來支持一個安全的或不安全的隨機源,很不幸的是,還沒有這樣的方法。也許在將來的迭代中......或者有個第三方會想出一個適配器來代替。(可惜這在我能力之上,很好地做好這件事情是相當困難的。)你幾乎可以輕松成功地派生隨機和覆蓋示例及下個字節 ......但目前還不清楚他們需要如何工作,甚至Sample可能會非常棘手。 也許下一次...
這是一篇國外的文章,被我翻譯過來。原文地址:http://csharpindepth.com/Articles/Chapter12/Random.aspx
接受批評指正,拒絕無腦噴糞。