System.Random
類表示僞随機數生成器,這是一種能夠産生滿足某些随機性統計要求的數字序列的算法。
var rand = new Random(); var i = rand.Next(); Console.WriteLine(i);
如果要在多線程環境下使用上述代碼:
Parallel.For(0, 10, x => { var rand = new Random(); var i = rand.Next(); Console.WriteLine(i); });
在 .NET Framework 平台上,會産生相同的輸出(即所有的随機結果都是相同的):
如果目标平台是 .NET 6.0 及以上,可以用以下代碼替代:
Parallel.For(0, 10, x => { var rand = Random.Shared; var i = rand.Next(); Console.WriteLine(i); });
此時,輸出結果是符合預期的。
實際上,如果在 .NET 6.0 平台上即使每次都建立新的 Random 對象,輸出結果也是符合預期的:
為了可以在 .NET Framework 及 .NET 6.0 之前的平台上擷取正确的輸出, 可以先嘗試在多個線程中共用一個 Random 對象:
Console.WriteLine("Dotnet Version: " + Environment.Version); var rand = new Random(); Parallel.For(0, 10, x => { var i = rand.Next(); Console.WriteLine(i); });
這種方式似乎可以,因為小規模測試獲得了正确的結果:
以下代碼建立了 10 個并發,并在每個并發中嘗試擷取了 10000 個随機數。然後将結果為零的資料統計出來:
Console.WriteLine("Dotnet Version: " + Environment.Version); var rand = new Random(); Parallel.For(0, 10, _ => { var numbers = new int[10_000]; for (int i = 0; i < numbers.Length; ++i) { numbers[i] = rand.Next(); //擷取 10000 個随機數,引發線程安全問題。 } var numZeros = numbers.Count(x => x == 0); // 統計異常資料 Console.WriteLine($"得到 {numZeros} 個為零的結果"); });
運作結果如下:
這表明:System.Random 在高并發下确實出現了異常。如果使用了 Release 釋出,則異常結果會更多。
如果我們願意用自己的類型來包裝
Random
,我們可以建立一個更好的解決方案。在下面的示例中,我們使用
[ThreadStatic]
為每個線程建立一個
Random
執行個體。這意味着我們重複使用
Random
執行個體,但是所有的通路總是來自單線程,是以保證了線程安全。這樣我們最多建立
n
個執行個體,其中
n
是線程數。
using System; internal static class ThreadLocalRandom { [ThreadStatic] private static Random _local; public static Random Instance { get { if (_local is ) { _local = new Random(); } return _local; } } }
使用方式如下:
Console.WriteLine("Dotnet Version: " + Environment.Version); Parallel.For(0, 10, _ => { var numbers = new int[10_000]; for (int i = 0; i < numbers.Length; ++i) { numbers[i] = ThreadLocalRandom.Instance.Next(); //擷取 10000 個随機數,引發線程安全問題。 } var numZeros = numbers.Count(x => x == 0); // 統計異常資料 Console.WriteLine($"得到 {numZeros} 個為零的結果"); });
請注意,在這個簡單的示例中,如果線上程之間傳遞
ThreadLocalRandom.Instance
,仍然有可能遇到線程安全問題。例如,下面顯示了與之前相同的問題:
Console.WriteLine("Dotnet Version: " + Environment.Version); var rand = ThreadLocalRandom.Instance; Parallel.For(0, 10, _ => { var numbers = new int[10_000]; for (int i = 0; i < numbers.Length; ++i) { numbers[i] = rand.Next(); //擷取 10000 個随機數,引發線程安全問題。 } var numZeros = numbers.Count(x => x == 0); // 統計異常資料 Console.WriteLine($"得到 {numZeros} 個為零的結果"); });
一個簡單的方案就是隐藏 Random 執行個體,僅暴露 Next 方法:
using System; internal static class ThreadSafeRandom { [ThreadStatic] private static Random _local; private static Random Instance { get { if (_local is ) { _local = new Random(); } return _local; } } public static int Next() => Instance.Next(); }
但這仍然無法解決在 .NET Framework 上因為系統時鐘的分辨率過低造成的重複值問題:
可以看到 1147840056 重複出現了很多次。
要解決該問題,隻要在建立
Random
對象時,指定不同的随機種子即可:
using System; internal static class ThreadSafeRandom { [ThreadStatic] private static Random _local; private static readonly Random Global = new Random(); // 全局執行個體,用于生成随機種子 private static Random Instance { get { if (_local is ) { int seed; lock (Global) // 確定 Global 不會被并發通路 { seed = Global.Next(); } _local = new Random(seed); } return _local; } } public static int Next() => Instance.Next(); }
如果您使用的是 .NET 6+ ,我仍然建議您使用内置的
Random.Shared
,但如果您沒有那麼幸運,您可以使用
ThreadSafeRandom
來解決您的問題。
如果您的目标是 .NET 6 和其他架構,您可以使用 #if 指令将您的 .NET 6 實作委托給
Random.Shared
,進而保持調用幹淨。