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