之前的博客 將時間作為GUID的方法 中,我使用了鎖。我在實際的使用中,錯將鎖的釋放放在了if語句中,這純粹是我的失誤,導致了很嚴重的錯誤。因此我在想是否有無鎖的將時間作為GUID的方式,答案是使用Interlocked中的 CompareExchange方法,該方法是原子操作。說是無鎖操作,其實就 ...
之前的博客 將時間作為GUID的方法 中,我使用了鎖。我在實際的使用中,錯將鎖的釋放放在了if語句中,這純粹是我的失誤,導致了很嚴重的錯誤。因此我在想是否有無鎖的將時間作為GUID的方式,答案是使用Interlocked中的 CompareExchange方法,該方法是原子操作。說是無鎖操作,其實就是讓clr來保證操作的原子性,而不用自己寫鎖。沒有鎖,也就沒有死鎖的風險了(當然CLR也可能犯錯,但是CLR犯錯要比我犯錯的概率低太多)。
我在 將時間作為GUID的方法 中介紹了,DateTime.Now提供不了ms級的時間,雖然時間的最小單位是ms,但是該時間的誤差至少在100ms以上,因此當多線程同時調用DateTime.Now.ToString("yyyyMMddHHmmssfff")的時候,如果在100ms內同時併發,就會出現同樣的結果,而GUID是不能相同的,因此需要對該方法進行簡單的改造,從而保證同一進程內,多線程訪問時,時間GUID的唯一性。有了之前死鎖的教訓,我決定不適用鎖來實現。該方法的實現方式是一種樂觀併發的模式,《CLR via C#》中多線程部分有介紹。我的想法是,既然DateTime.now有誤差,我可以在後面添加一個數字,這個數字每次會+1,這樣無論多少個線程訪問,都不會重覆。代碼如下:
static int c = 0; public static string GetTimeUtils() { //這樣做無法保證f的唯一性,因為其他線程在調用該方法時,有可能讀取了相同的c, //從而f++得到相同結果//return DateTime.Now.ToString("yyyyMMddHHmmssfff") + c++; int f,z; do { f = c; z = f+1; if (z >= 9999) z = 0; } while (Interlocked.CompareExchange(ref c, z, f) != f); return DateTime.Now.ToString("yyyyMMddHHmmssfff") + f; }
先解釋下Interlocked.CompareExchange方法,該方法是原子操作,意思是如果c==f,則c=z,然後返回c原來的值。將其放進while迴圈中,是為了保證f讀取的c是最新的值。如果f讀到的值不是最新的值,就表明這期間有其他線程對c++,這時就重新計算。這其實相當於一次“原子操作”,只不過,這個原子操作不是利用鎖來獲得的,而是利用線程執行間歇來恰巧獲得的。這有點像自旋鎖的意思,都有一個while迴圈。如果在執行期間有其他線程也對c++,那麼重新來,直到找到了只有一個線程執行的間歇。這樣解釋下來,這個方法還真是很“樂觀”。如果方法的執行時間較長,併發數較高,這樣的間歇是非常不好找的,也就不適用這種無鎖模式。該方法適合需要同步的代碼量較小,執行時間非常短的情況。
總結一下,該方法的想法是將f=c++原子化,辦法是找到多線程的間歇。
下麵我們來驗證一下該方法是否能夠保證唯一性:
class Program { static void Main(string[] args){ for (int i = 0; i < 20; i++){ ConcurrentBag<string> list = new ConcurrentBag<string>(); TaskExtension.ParallelRun(9000, true, () => list.Add(Test.GetTimeUtils())) .Then(() => Console.WriteLine($"是否有重覆?{list.Count() != list.Distinct().Count()}")); } Console.Read(); } } public static class TaskExtension { public static Task[] ParallelRun(int runCount, bool start, Action action){ var tasks = new Task[runCount]; for (int i = 0; i < runCount; i++){ tasks[i] = new Task(action); if (start) tasks[i].Start(); } return tasks; } public static async Task Then(this Task[] t, Action action){ await Task.WhenAll(t); action(); } }
可以看到,是否有重覆,結果為false,證明不會產生重覆。
我也對該方法和帶鎖的方法進行了對比,發現該方法和有鎖版在性能上基本沒有優勢。我使用的是lock,該鎖的後臺實現是Monitor,是一個混合鎖,在較短時間的時候,會先自旋,因此性能較好。但是一旦使用鎖,就有被死鎖的風險,而無鎖版是不用擔心這個問題的。也推薦大家去看看Interlocked中的方法,裡面提供了一些簡單的原子操作。
以上為實現以時間作為GUID的方法和測試代碼,歡迎有疑問的小伙伴在評論區和我討論。