2016-11-30 54 views
4

在我的下面的代碼中,我鎖定了guid,嘗試使其線程安全。 用我的示例應用程序,我會每運行10次程序就會得到一個「重複密鑰」。又名,我得到一份重複,這不是我所需要的。構建一個線程安全的GUID增量'

無論如何要讓「.NextGuid」線程安全嗎?

using System;  
namespace MyConsoleOne.BAL 
{ 
    public class GuidStore 
    { 
     private static object objectlock = new object();  
     private Guid StartingGuid { get; set; }  
     private Guid? LastGuidHolder { get; set; }  
     public GuidStore(Guid startingGuid) 
     { 
      this.StartingGuid = startingGuid; 
     } 

     public Guid? GetNextGuid() 
     { 
      lock (objectlock) 
      { 
       if (this.LastGuidHolder.HasValue) 
       { 
        this.LastGuidHolder = Increment(this.LastGuidHolder.Value); 
       } 
       else 
       { 
        this.LastGuidHolder = Increment(this.StartingGuid); 
       } 
      }  
      return this.LastGuidHolder; 
     } 

     private Guid Increment(Guid guid) 
     {  
      byte[] bytes = guid.ToByteArray();  
      byte[] order = { 15, 14, 13, 12, 11, 10, 9, 8, 6, 7, 4, 5, 0, 1, 2, 3 };  
      for (int i = 0; i < 16; i++) 
      { 
       if (bytes[order[i]] == byte.MaxValue) 
       { 
        bytes[order[i]] = 0; 
       } 
       else 
       { 
        bytes[order[i]]++; 
        return new Guid(bytes); 
       } 
      }  
      throw new OverflowException("Guid.Increment failed."); 
     } 
    } 
} 

using MyConsoleOne.BAL; 
using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Threading.Tasks; 

namespace MyConsoleOne 
{ 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      GuidStore gs = new GuidStore(Guid.NewGuid()); 

      for (int i = 0; i < 1000; i++) 
      { 
       Console.WriteLine(i); 
       Dictionary<Guid, int> guids = new Dictionary<Guid, int>(); 
       Parallel.For(0, 1000, j => 
       { 
        Guid? currentGuid = gs.GetNextGuid(); 
        guids.Add(currentGuid.Value, j); 
        Console.WriteLine(currentGuid); 
       }); // Parallel.For 
      }  
      Console.WriteLine("Press ENTER to Exit"); 
      Console.ReadLine(); 
     } 
    } 
} 

我的代碼的組合:

因爲我得到 「爲什麼不使用Guid.NewGuid」 的問題,我將提供原因在這裏:

我有一個父進程,有一個唯一的事由Guid.NewGuid()創建的ifier。我將這稱爲「父母指導」。該父進程將創建N個文件。如果我是從頭開始寫的,我只會在文件名末尾附加「N」。因此,如果父母的GUID「11111111-1111-1111-1111-111111111111」例如,我將通過現有的「合同」與客戶端寫入文件

"11111111-1111-1111-1111-111111111111_1.txt" 
"11111111-1111-1111-1111-111111111111_2.txt" 
"11111111-1111-1111-1111-111111111111_3.txt" 

,等等,等等。然而,:::文件名必須有一個(唯一的)Guid,並且文件名中沒有「N」(1,2等等)值(這個「契約」已經存在多年了,所以它的設置幾乎是一成不變的)。通過這裏列出的功能,我可以保留「合同」,但是文件名與「父」Guid鬆散關聯(也是由Guid.NewGuid()生成的父項)。碰撞是不是與文件名有關的問題(它們被置於一個獨立的文件夾中,用於'進程'執行)。碰撞是「父母」Guid的問題。但是,這一點已經在Guid.NewGuid中處理過了。

所以用「11111111-1111-1111-1111-111111111111」起始的Guid,我就可以寫出這樣的文件名:

OTHERSTUFF_111111111-1111-1111-1111-111111111112_MORESTUFF.txt 
OTHERSTUFF_111111111-1111-1111-1111-111111111113_MORESTUFF.txt 
OTHERSTUFF_111111111-1111-1111-1111-111111111114_MORESTUFF.txt 
OTHERSTUFF_111111111-1111-1111-1111-111111111115_MORESTUFF.txt 
OTHERSTUFF_111111111-1111-1111-1111-111111111116_MORESTUFF.txt 
OTHERSTUFF_111111111-1111-1111-1111-111111111117_MORESTUFF.txt 
OTHERSTUFF_111111111-1111-1111-1111-111111111118_MORESTUFF.txt 
OTHERSTUFF_111111111-1111-1111-1111-111111111119_MORESTUFF.txt 
OTHERSTUFF_111111111-1111-1111-1111-11111111111a_MORESTUFF.txt 
OTHERSTUFF_111111111-1111-1111-1111-11111111111b_MORESTUFF.txt 
在我的例子

所以上面,「父GUID」由「this.StartingGuid」代表......然後我得到「遞增」guid的結果。

還有。我可以編寫更好的單元測試,因爲現在我會提前知道文件名。

附加:

最終代碼版本:

public class GuidStore 
{ 
    private static object objectlock = new object(); 

    private static int[] byteOrder = { 15, 14, 13, 12, 11, 10, 9, 8, 6, 7, 4, 5, 0, 1, 2, 3 }; 

    private Guid StartingGuid { get; set; } 

    private Guid? LastGuidHolder { get; set; } 

    public GuidStore(Guid startingGuid) 
    { 
     this.StartingGuid = startingGuid; 
    } 

    public Guid GetNextGuid() 
    { 
     return this.GetNextGuid(0); 
    } 

    public Guid GetNextGuid(int firstGuidOffSet) 
    { 
     lock (objectlock) 
     { 
      if (this.LastGuidHolder.HasValue) 
      { 
       this.LastGuidHolder = Increment(this.LastGuidHolder.Value); 
      } 
      else 
      { 
       this.LastGuidHolder = Increment(this.StartingGuid); 
       for (int i = 0; i < firstGuidOffSet; i++) 
       { 
        this.LastGuidHolder = Increment(this.LastGuidHolder.Value); 
       } 
      } 

      return this.LastGuidHolder.Value; 
     } 
    } 

    private static Guid Increment(Guid guid) 
    { 
     var bytes = guid.ToByteArray(); 
     var canIncrement = byteOrder.Any(i => ++bytes[i] != 0); 
     return new Guid(canIncrement ? bytes : new byte[16]); 
    } 
} 

和單元測試:

public class GuidStoreUnitTests 
{ 
    [TestMethod] 
    public void GetNextGuidSimpleTest() 
    { 
     Guid startingGuid = new Guid("11111111-1111-1111-1111-111111111111"); 
     GuidStore gs = new GuidStore(startingGuid); 


     List<Guid> guids = new List<Guid>(); 

     const int GuidCount = 10; 

     for (int i = 0; i < GuidCount; i++) 
     { 
      guids.Add(gs.GetNextGuid()); 
     } 

     Assert.IsNotNull(guids); 
     Assert.AreEqual(GuidCount, guids.Count); 
     Assert.IsNotNull(guids.FirstOrDefault(g => g == new Guid("11111111-1111-1111-1111-111111111112"))); 
     Assert.IsNotNull(guids.FirstOrDefault(g => g == new Guid("11111111-1111-1111-1111-111111111113"))); 
     Assert.IsNotNull(guids.FirstOrDefault(g => g == new Guid("11111111-1111-1111-1111-111111111114"))); 
     Assert.IsNotNull(guids.FirstOrDefault(g => g == new Guid("11111111-1111-1111-1111-111111111115"))); 
     Assert.IsNotNull(guids.FirstOrDefault(g => g == new Guid("11111111-1111-1111-1111-111111111116"))); 
     Assert.IsNotNull(guids.FirstOrDefault(g => g == new Guid("11111111-1111-1111-1111-111111111117"))); 
     Assert.IsNotNull(guids.FirstOrDefault(g => g == new Guid("11111111-1111-1111-1111-111111111118"))); 
     Assert.IsNotNull(guids.FirstOrDefault(g => g == new Guid("11111111-1111-1111-1111-111111111119"))); 
     Assert.IsNotNull(guids.FirstOrDefault(g => g == new Guid("11111111-1111-1111-1111-11111111111a"))); 
     Assert.IsNotNull(guids.FirstOrDefault(g => g == new Guid("11111111-1111-1111-1111-11111111111b"))); 
    } 

    [TestMethod] 
    public void GetNextGuidWithOffsetSimpleTest() 
    { 
     Guid startingGuid = new Guid("11111111-1111-1111-1111-111111111111"); 
     GuidStore gs = new GuidStore(startingGuid); 

     List<Guid> guids = new List<Guid>(); 

     const int OffSet = 10; 

     guids.Add(gs.GetNextGuid(OffSet)); 

     Assert.IsNotNull(guids); 
     Assert.AreEqual(1, guids.Count); 

     Assert.IsNotNull(guids.FirstOrDefault(g => g == new Guid("11111111-1111-1111-1111-11111111111c"))); 
    } 

    [TestMethod] 
    public void GetNextGuidMaxRolloverTest() 
    { 
     Guid startingGuid = new Guid("ffffffff-ffff-ffff-ffff-ffffffffffff"); 
     GuidStore gs = new GuidStore(startingGuid); 

     List<Guid> guids = new List<Guid>(); 

     const int OffSet = 10; 

     guids.Add(gs.GetNextGuid(OffSet)); 

     Assert.IsNotNull(guids); 
     Assert.AreEqual(1, guids.Count); 

     Assert.IsNotNull(guids.FirstOrDefault(g => g == Guid.Empty)); 
    } 

    [TestMethod] 
    public void GetNextGuidThreadSafeTest() 
    { 
     Guid startingGuid = Guid.NewGuid(); 
     GuidStore gs = new GuidStore(startingGuid); 

     /* The "key" of the ConcurrentDictionary must be unique, so this will catch any duplicates */ 
     ConcurrentDictionary<Guid, int> guids = new ConcurrentDictionary<Guid, int>(); 
     Parallel.For(
      0, 
      1000, 
      j => 
      { 
       Guid currentGuid = gs.GetNextGuid(); 
       if (!guids.TryAdd(currentGuid, j)) 
       { 
        throw new ArgumentOutOfRangeException("GuidStore.GetNextGuid ThreadSafe Test Failed"); 
       } 
      }); // Parallel.For 
    } 

    [TestMethod] 
    public void GetNextGuidTwoRunsProduceSameResultsTest() 
    { 
     Guid startingGuid = Guid.NewGuid(); 

     GuidStore gsOne = new GuidStore(startingGuid); 

     /* The "key" of the ConcurrentDictionary must be unique, so this will catch any duplicates */ 
     ConcurrentDictionary<Guid, int> setOneGuids = new ConcurrentDictionary<Guid, int>(); 
     Parallel.For(
      0, 
      1000, 
      j => 
      { 
       Guid currentGuid = gsOne.GetNextGuid(); 
       if (!setOneGuids.TryAdd(currentGuid, j)) 
       { 
        throw new ArgumentOutOfRangeException("GuidStore.GetNextGuid ThreadSafe Test Failed"); 
       } 
      }); // Parallel.For 

     gsOne = null; 

     GuidStore gsTwo = new GuidStore(startingGuid); 

     /* The "key" of the ConcurrentDictionary must be unique, so this will catch any duplicates */ 
     ConcurrentDictionary<Guid, int> setTwoGuids = new ConcurrentDictionary<Guid, int>(); 
     Parallel.For(
       0, 
       1000, 
       j => 
       { 
        Guid currentGuid = gsTwo.GetNextGuid(); 
        if (!setTwoGuids.TryAdd(currentGuid, j)) 
        { 
         throw new ArgumentOutOfRangeException("GuidStore.GetNextGuid ThreadSafe Test Failed"); 
        } 
       }); // Parallel.For 

     bool equal = setOneGuids.Select(g => g.Key).OrderBy(i => i).SequenceEqual(
         setTwoGuids.Select(g => g.Key).OrderBy(i => i), new GuidComparer<Guid>()); 

     Assert.IsTrue(equal); 
    } 
} 

internal class GuidComparer<Guid> : IEqualityComparer<Guid> 
{ 
    public bool Equals(Guid x, Guid y) 
    { 
     return x.Equals(y); 
    } 

    public int GetHashCode(Guid obj) 
    { 
     return 0; 
    } 
} 
+2

那豈不是更容易得到一個隨機GUID而不是在增加?鑑於你計劃使用多線程的事實,有兩個線程生成相同的隨機引導的可能性比兩個線程從1遞增到2的可能性要小。 – Lidaranis

+4

有沒有原因'return'在'lock'之外?看起來像是其中一個線程在返回之前暫停,並且下一個線程被允許進入關鍵部分,後者修改該變量並返回相同的值。 –

+4

你爲什麼不使用Guid.NewGuid()?使用.NET給你的工具。 –

回答

6

您這裏有兩個問題:

  1. Dictionary.Add()不是線程安全的。改爲使用ConcurrentDictionary.TryAdd()
  2. GetNextGuid()的實現有競爭條件,因爲您在鎖外部返回this.LastGuidHolder,所以它可能會在返回之前由另一個線程修改。

一個顯而易見的解決方案是將鎖內的回報:

public Guid? GetNextGuid() 
{ 
    lock (objectlock) 
    { 
     if (this.LastGuidHolder.HasValue) 
     { 
      this.LastGuidHolder = Increment(this.LastGuidHolder.Value); 
     } 
     else 
     { 
      this.LastGuidHolder = Increment(this.StartingGuid); 
     } 

     return this.LastGuidHolder; 
    } 
} 

不過,我想返回類型更改爲Guid - 它似乎並沒有任何目的,以返回Guid? - 在這個時候,應該被隱藏在類中:

public Guid GetNextGuid() 
{ 
    lock (objectlock) 
    { 
     if (this.LastGuidHolder.HasValue) 
     { 
      this.LastGuidHolder = Increment(this.LastGuidHolder.Value); 
     } 
     else 
     { 
      this.LastGuidHolder = Increment(this.StartingGuid); 
     } 

     return this.LastGuidHolder.Value; 
    } 
} 

下面是使用ConcurrentDictionary測試方法的版本:

static void Main(string[] args) 
{ 
    GuidStore gs = new GuidStore(Guid.NewGuid()); 

    for (int i = 0; i < 1000; i++) 
    { 
     Console.WriteLine(i); 
     ConcurrentDictionary<Guid, int> guids = new ConcurrentDictionary<Guid, int>(); 
     Parallel.For(0, 1000, j => 
     { 
      Guid currentGuid = gs.GetNextGuid(); 
      if (!guids.TryAdd(currentGuid, j)) 
      { 
       Console.WriteLine("Duplicate found!"); 
      } 
     }); // Parallel.For 
    } 

    Console.WriteLine("Press ENTER to Exit"); 
    Console.ReadLine(); 
} 

說了這麼多,我可你爲什麼不只是使用Guid.NewGuid()不明白...

+1

+5,000,000,000,使用'Guid.NewGuid()' – Jesper

+2

該字典只是測試我的代碼的快速方法。它不是「生產代碼」。但我會換成線程安全字典進行測試。是的,在上班時我意識到我應該把返回值放在鎖內。我一直使用Guid.NewGuid()。我有一個獨特的警告,我有一個由Guid.NewGuid()生成的「父」Guid。它會有幾個孩子(XML文件)。我試圖讓文件名鬆散地匹配「父」Guid。它還有助於單元測試,因爲我可以稍微預測文件名。 – granadaCoder

+0

這就是我在4:30上午寫代碼的原因.......感謝您的快速幫助。 – granadaCoder