[翻譯].NET隨機數


原文鏈接:http://csharpindepth.com/Articles/Chapter12/Random.aspx

 

隨機數

當你在Stack Overflow上看到看到某個問題標題當中有“隨機”這個詞,你幾乎能夠肯定這和其他很多問題類似的基礎的問題。這篇文章講述了為什么隨機這個概念引起了這么多的問題,以及如何去解決它們。

問題

Stack Overflow上的問題通常是這樣的:

我使用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);
}

這到底發生了什么?

解釋

Random類並不是一個真正的隨機數生成器,而是一個偽隨機數生成器。任何一個Random實例都有一定數量的狀態值,當你調用Next方法時,實例會用這些狀態值給你返回一些看上去是隨機的數據。然后將內部的狀態進行改變,然后下一次你就能夠拿到下一組看上去隨機的數。
所有的這些都是已經決定了的。如果你用同一個初始的狀態值(可以通過種子來提供)去創建Random實例,然后調用這個實例的同樣的方法,那么你將會拿到同樣的數據。
那么,我們的演示代碼到底哪里錯了?我們在每一個循環中都創建一個新的Random實例。Random默認的構造函數會拿當前的日期和時間當作種子,在內部的當前日期和時間改變之前,你一般已經執行了很多代碼了。所以我們一直重復地在使用同一個種子(譯者注:因為Random默認的種子是最后一個計算機啟動到目前的毫秒數,是毫秒級別的,所以多次調用情況下很容易出現同個種子的情況),然后重復地取到相同的結果。

我們該怎么辦?

對於這個問題我們有很多的解決方案 - 有一些解決方案會優於其他的。讓我們首先來看看其中的一個解決方案,這個方案和其他的都不一樣。

使用加密隨機數生成器

.NET框架有一個RandomNumberGenerator類,這是一個抽象類,所有的加密隨機數生成器都必須繼承自這個類。框架本身提供了一個派生類:RNGCryptoServiceProvider。加密隨機數生成器的主要思想是,即使它是一個偽隨機數的生成器,但是它做了很多工作是的其產生的隨機數無法預測。內置的實現使用了很多能夠有效代表你計算機的“噪聲”的熵值,這就讓隨機數變得無法預測。(譯者注:這里說的噪聲可能是用戶的鼠標軌跡、用戶按鍵盤的位置等無法預測的東西)“噪聲”可能不僅僅被用來產生一個種子,也可能在生成下一個隨機數的時候被使用,所以即使你知道了當前的狀態,你還是不足以去預測下一個結果。Windows系統還可以使用特定硬件上的隨機性(比如說某個可以監測放射性同位素衰變的硬件)來確保隨機數生成器更加的安全。

我們再拿Random類跟加密隨機數生成器相比,如果你有10個Random.Next(100)返回的結果,並且你有足夠的計算能力的話,你可能可以破解出原始的種子的值從而預測出下一個隨機值。而且之前產生的隨機值也可以得到。如果這些機制被用在一些安全或者金融用途上,這將可能是災難性的。加密隨機數生成器一般來說會比Random要慢,但是在產生獨立無法預測的隨機數這點上要比Random好得多。

很多情況下隨機數生成器的效率並不是問題,友好的接口(API)才是問題。RandomNumberGenerator類被設計成生成隨機的字節,而且只能以字節的形式。再看看Random類提供的接口則要友好許多,它允許生成一個隨機的整數,隨機的浮點數,或一些隨機的字節。在使用RandomNumberGenerator時,我常常會發現我需要在一個區間內產生一個隨機值,然后想要從一個隨機的字節數組中可靠一致地獲得這么一個隨機值是很困難的。不是說不可能,但是你至少需要在RandomNumberGenerator上面在封裝一層適配器類(Adapter Class)。在多數情況下,Random所產生的偽隨機數就已經夠用了,不過你要小心不要掉入前面提到的一些“陷阱”。下面就讓我們看看如何才能避免“陷阱”。

重復使用單個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實例,並且在各處都是用這個實例。另一種方案是我們將Random的構造點放在一個更高層次的地方,當然這樣子最終你會到達程序的入口。這樣子我們就只會創建一個Random實例,然后將其傳到各個需要使用的地方。這是一個不錯的注意(而且能夠很好的表達出依賴關系),但是這個方案並不能完整的工作,如果你代碼使用多個線程的情況下這就會出問題。

線程安全問題

Random並非線程安全的。這確實是一個很大的硬傷,我們理想的情況是在程序里只存在一個實例並且在各個地方使用它。但是這樣子是不行的!如果你在多個線程同時使用同一個實例的話,它很可能會將其內部的狀態都變成0,這樣子的話我們的Random實例就沒有用了。

同樣的,解決這個問題有兩個方案。一個是仍然使用一個實例,但是利用加鎖的方法來確保同時只有一個線程調用,每一個調用的地方都必須獲得鎖以后才能使用隨機數生成器。我們可以封裝一層使得調用者簡單地使用。但是如果是在一個頻繁使用多線程的系統里,這樣很可能會浪費很多時間在等待鎖上。

另外一個方案 - 一個線程只使用一個實例。我們需要做的是確保我們創建的實例不會重用同一個種子(也就是說我們不能直接調用默認的無參構造函數),這個方案相對來說還是比較簡單明了的。

安全提供者

幸運的是,.NET 4中新增的ThreadLocal<T>類可以幫助我們非常方便的寫出一個提供者來確保每個線程都有一個單一的實例。你只需要簡單的提供給ThreadLocal<T>的構造函數一個委托讓它去通過這個委托去獲取一個T的實例,然后接下來的事情就是.NET框架幫你做了。在我的這個例子里,我選擇使用一個種子變量,將其初始化值設成 Environment.TickCount(和默認的無參構造函數是一樣的),然后每次調用完以后自增,這樣子就可以保證每個線程都使用不同的種子。

看如下代碼,這個類是一個靜態類,只有一個公有的方法:GetThreadRandom。把它設計成一個方法而不是直接暴露屬性主要是為了方便,這樣子需要產生隨機數的地方只需要引用Random類本身而不需要去引用Func<Random>。如果類型設計的時候只是用於單線程操作的話,Provider會調用委托來獲得實例然后之后就會重用這個實例。如果Provider被多個線程使用的話,它每次都會去會調用委托來獲得實例。ThreadLocal只會為每個線程創建一個實例,並且每個Random實例都會是用一個不同的種子。當需方法其傳到依賴的是,我們可以是用一個方法的轉換:new TypeThatNeedsRandom(RandomProvider.GetThreadRandom)

代碼如下:

using System;
using System.Threading;

public static class RandomProvider
{    
    private static int seed = Environment.TickCount;

    private static ThreadLocal randomWrapper = new ThreadLocal(() =>
        new Random(Interlocked.Increment(ref seed))
    );

    public static Random GetThreadRandom()
    {
        return randomWrapper.Value;
    }
}

很簡單是不是?這是因為這個類所關注的僅僅是提供正確的Random實例,它並不關心你拿到實例以后調用了什么方法或者是做了什么事情。當然這個類也有可能被誤用,比如說拿到一個Random實例以后將其用到多線程的環境中,這個我們無法避免,但是這個類讓我們更加容易的去做正確的事情。

接口(Interface)設計的問題

仍然有一個問題:它還是不夠安全。就像我之前所說的,框架確實有更加安全的版本:RandomNumberGenerator,最常用的衍生類是RNGCryptoServiceProvider。但是,它提供的API在一般的場景下確實是非常之難用。

如果框架提供者能夠區分出“隨機源”的概念和“我要簡單的獲得一個隨機數”的概念,我會很高興。這樣子我們就可以調用簡單的API來生成隨機數,然后再根據需求來選擇是否需要使用安全的隨機數源。但是,很遺憾,現實不是這樣子的,或許在未來的版本里,又或許某一個第三方類庫會提供一個適配器來取代之。(很遺憾,這已經超越我的能力了,最類似的這種事情是非常之困難的)。你可以通過繼承Random然后重載SampleNextBytes方法來實現更安全的版本,但是這明顯不是框架設計這應該去做的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM