C#中的隨機數

前些日子, 收到一個小需求需要隨機產生一組帶大小寫字母和數字的亂數字串, 想說需求滿簡單的, 快速寫一下就寫完commit了, 然後過不久就爆掉了.
來看看究竟寫了些什麼鬼東西~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class RandomUtil
{
private string _charDic = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

public string RandomString(int length)
{
Random rdm = new Random();
string result = string.Empty;
for (int i = 0; i < length; i++)
{
int nextIndex = rdm.Next(_charDic.Length);
result += _charDic[nextIndex];
}
return result;
}
}

程式內容很單純, 就是用System.Random隨機生出指定長度的字串, 但問題就出在需要隨機產生兩組, 於是呼叫端就呼叫了兩次, 像是這樣:

1
2
3
var rdm = new RandomUtil();
Console.WriteLine(rdm.RandomString(10));
Console.WriteLine(rdm.RandomString(10));

然後產生的兩個結果字串一模一樣, Why?

同時建立多個Random

Random的產生方式是基於一個種子來產生的, 也就是public Random(int Seed)中的Seed, 也就是說如果種子一樣, 那兩個new出來的Random物件產生的隨機數是一模一樣的, 而另外一個不帶參數的建構子呢?
referencesource.microsoft.com上面可以查到原始碼是這樣的:

1
2
3
4
public Random() 
: this(Environment.TickCount)
{
}

是的,預設以Environment.TickCount做為種子, 而Environment.TickCount是衍生自系統計時器的一個值.

因為Random()的亂數是基於系統計時器產生的, 所以如果在極短時間內 (Environment.TickCount相同) 建立多個Random實例,就會導致產生的亂數是一樣的.

知道問題後, 腦中閃過兩個做法:

解一: Thread.Sleep()

new Random()之前, 先延時一毫秒, 避免拿到重複的時間, 雖然直覺但我個人不喜歡.

解二: 建立唯一的Random

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class RandomUtil
{
private string _charDic = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

private Random _rdm = new Random();

public string RandomString(int length)
{
string result = string.Empty;
for (int i = 0; i < length; i++)
{
int nextIndex = _rdm.Next(_charDic.Length);
result += _charDic[nextIndex];
}
return result;
}
}

RandomUtil初始化的時候就建立一個唯一的Random物件, 避免短時間內重複建立, 呼叫端程式碼不變, 這次兩次產生的結果是一樣的了,問題在大部分的情境下解決了.

多執行緒環境下的Random

考慮同時有兩條執行緒都建立了Random, 簡單比對一下結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
string firstThreadResult = null;
var firstThread = new Thread(new ThreadStart(() =>
{
firstThreadResult = new RandomUtil().RandomString(10);
}));

string secondThreadResult = null;
var secondThread = new Thread(new ThreadStart(() =>
{
secondThreadResult = new RandomUtil().RandomString(10);
}));

firstThread.Start();
secondThread.Start();

firstThread.Join();
secondThread.Join();

var rdm = new RandomUtil();
Console.WriteLine(firstThreadResult);
Console.WriteLine(secondThreadResult);

實驗結果顯示, 兩個產生的字串一樣, 所以在多執行緒的情境下, 還是有機會產生重複的亂數組合.

解三: RNGCryptoServiceProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class RandomUtil
{
private string _charDic = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
private RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider();

public string RandomString(int length)
{
string resultArr = string.Empty;

for (int i = 0; i < length; i++)
{
var nextBytes = new byte[4];
_rng.GetBytes(nextBytes);

var index = BitConverter.ToInt32(nextBytes, 0) % _charDic.Length;
resultArr += _charDic[index];
}

return resultArr;
}
}

RNGCryptoServiceProvider可以避免Random在多執行緒情境下的重複問題, 但缺點就是他不像Random提供那麼多方法, 所以需要自己實作Next(),Next(max),Next(min, max)等方法.

後來我把相關方法整理重構過放在我的 Github 上了, 實作細節有不少差異, 但概念是跟上面的範例一樣的.

延伸 - 關於隨機數

密碼學(隨機數筆記)

參考

Random numbers - C# in depth