天天看點

如何在 .NET Framework 中實作一個線程安全的 System.Random 對象?

作者:opendotnet

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 Framework 中實作一個線程安全的 System.Random 對象?

如果目标平台是 .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 中實作一個線程安全的 System.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);              });           

這種方式似乎可以,因為小規模測試獲得了正确的結果:

如何在 .NET Framework 中實作一個線程安全的 System.Random 對象?

以下代碼建立了 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} 個為零的結果");              });           

運作結果如下:

如何在 .NET Framework 中實作一個線程安全的 System.Random 對象?

這表明: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 上因為系統時鐘的分辨率過低造成的重複值問題:

如何在 .NET Framework 中實作一個線程安全的 System.Random 對象?

可以看到 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

,進而保持調用幹淨。