多執行緒下解決資源競爭的7種方法

  • 2019 年 10 月 3 日
  • 筆記

前言

  一般情況下,只要涉及到多執行緒編程,程式的複雜性就會顯著上升,性能顯著下降,BUG出現的概率大大提升。

多執行緒編程本意是將一段程式並行運行,提升數據處理能力,但是由於大部分情況下都涉及到共有資源的競爭,所以修改資源

對象時必須加鎖處理。但是鎖的實現有很多種方法,下面就來一起了解一下在C#語言中幾種鎖的實現與其性能表現。

 

一、c#下的幾種鎖的運用方式

1、臨界區,通過對多執行緒的串列化來訪問公共資源或一段程式碼,速度快,適合控制數據訪問。

 1         private static object obj = new object();   2         private static int lockInt;   3         private static void LockIntAdd()   4         {   5             for (var i = 0; i < runTimes; i++)   6             {   7                 lock (obj)   8                 {   9                     lockInt++;  10                 }  11             }  12         }

你沒看錯,c#中的lock語法就是臨界區(Monitor)的一個語法糖,這大概是90%以上的.net程式設計師首先想到的鎖,不過大部分人都只是知道

有這麼個語法,不知道其實是以臨界區的方式處理資源競爭。

 

2、互斥量,為協調共同對一個共享資源的單獨訪問而設計的。

c#中有一個Mutex類,就在System.Threading命名空間下,Mutex其實就是互斥量,互斥量不單單能處理多執行緒之間的資源競爭,還能處理

進程之間的資源競爭,功能是比較強大的,但是開銷也很大,性能比較低。

 1         private static Mutex mutex = new Mutex();   2         private static int mutexInt;   3         private static void MutexIntAdd()   4         {   5             for (var i = 0; i < runTimes; i++)   6             {   7                 mutex.WaitOne();   8                 mutexInt++;   9                 mutex.ReleaseMutex();  10             }  11         }

 

3、訊號量,為控制一個具有有限數量用戶資源而設計。

 1         private static Semaphore sema = new Semaphore(1, 1);   2         private static int semaphoreInt;   3         private static void SemaphoreIntAdd()   4         {   5             for (var i = 0; i < runTimes; i++)   6             {   7                 sema.WaitOne();   8                 semaphoreInt++;   9                 sema.Release();  10             }  11         }

 

4、事   件:用來通知執行緒有一些事件已發生,從而啟動後繼任務的開始。

 1         public static AutoResetEvent autoResetEvent = new AutoResetEvent(true);   2         private static int autoResetEventInt;   3         private static void AutoResetEventIntAdd()   4         {   5             for (var i = 0; i < runTimes; i++)   6             {   7                 if (autoResetEvent.WaitOne())   8                 {   9                     autoResetEventInt++;  10                     autoResetEvent.Set();  11                 }  12             }  13         }

 

5、讀寫鎖,這種鎖允許在有其他程式正在寫的情況下讀取資源,所以如果資源允許臟讀,用這個比較合適

 1         private static ReaderWriterLockSlim LockSlim = new ReaderWriterLockSlim();   2         private static int lockSlimInt;   3         private static void LockSlimIntAdd()   4         {   5             for (var i = 0; i < runTimes; i++)   6             {   7                 LockSlim.EnterWriteLock();   8                 lockSlimInt++;   9                 LockSlim.ExitWriteLock();  10             }  11         }

 

6、原子鎖,通過原子操作Interlocked.CompareExchange實現“無鎖”競爭

 1         private static int isLock;   2         private static int ceInt;   3         private static void CEIntAdd()   4         {   5             //long tmp = 0;   6             for (var i = 0; i < runTimes; i++)   7             {   8                 while (Interlocked.CompareExchange(ref isLock, 1, 0) == 1) { Thread.Sleep(1); }   9  10                 ceInt++;  11                 Interlocked.Exchange(ref isLock, 0);  12             }  13         }

 

7、原子性操作,這是一種特例,野外原子性操作本身天生執行緒安全,所以無需加鎖

1         private static int atomicInt;  2         private static void AtomicIntAdd()  3         {  4             for (var i = 0; i < runTimes; i++)  5             {  6                 Interlocked.Increment(ref atomicInt);  7             }  8         }

 

8、不加鎖,如果不加鎖,那多執行緒下運行結果肯定是錯的,這裡貼上來比較一下性能

1         private static int noLockInt;  2         private static void NoLockIntAdd()  3         {  4             for (var i = 0; i < runTimes; i++)  5             {  6                 noLockInt++;  7             }  8         }

 

二、性能測試

1、測試程式碼,執行1000,10000,100000,1000000次

 1         private static void Run()   2         {   3             var stopwatch = new Stopwatch();   4             var taskList = new Task[loopTimes];   5   6             // 多執行緒   7             Console.WriteLine();   8             Console.WriteLine($"              執行緒數:{loopTimes}");   9             Console.WriteLine($"            執行次數:{runTimes}");  10             Console.WriteLine($"        校驗值應等於:{runTimes * loopTimes}");  11  12             // AtomicIntAdd  13             stopwatch.Restart();  14             for (var i = 0; i < loopTimes; i++)  15             {  16                 taskList[i] = Task.Factory.StartNew(() => { AtomicIntAdd(); });  17             }  18             Task.WaitAll(taskList);  19             Console.WriteLine($"{GetFormat("AtomicIntAdd")}, 總耗時:{stopwatch.ElapsedMilliseconds}毫秒, 校驗值:{atomicInt}");  20  21             // CEIntAdd  22             taskList = new Task[loopTimes];  23             stopwatch.Restart();  24  25             for (var i = 0; i < loopTimes; i++)  26             {  27                 taskList[i] = Task.Factory.StartNew(() => { CEIntAdd(); });  28             }  29             Task.WaitAll(taskList);  30             Console.WriteLine($"{GetFormat("CEIntAdd")}, 總耗時:{stopwatch.ElapsedMilliseconds}毫秒, 校驗值:{ceInt}");  31  32             // LockIntAdd  33             taskList = new Task[loopTimes];  34             stopwatch.Restart();  35  36             for (var i = 0; i < loopTimes; i++)  37             {  38                 taskList[i] = Task.Factory.StartNew(() => { LockIntAdd(); });  39             }  40             Task.WaitAll(taskList);  41             Console.WriteLine($"{GetFormat("LockIntAdd")}, 總耗時:{stopwatch.ElapsedMilliseconds}毫秒, 校驗值:{lockInt}");  42  43             // MutexIntAdd  44             taskList = new Task[loopTimes];  45             stopwatch.Restart();  46  47             for (var i = 0; i < loopTimes; i++)  48             {  49                 taskList[i] = Task.Factory.StartNew(() => { MutexIntAdd(); });  50             }  51             Task.WaitAll(taskList);  52             Console.WriteLine($"{GetFormat("MutexIntAdd")}, 總耗時:{stopwatch.ElapsedMilliseconds}毫秒, 校驗值:{mutexInt}");  53  54             // LockSlimIntAdd  55             taskList = new Task[loopTimes];  56             stopwatch.Restart();  57  58             for (var i = 0; i < loopTimes; i++)  59             {  60                 taskList[i] = Task.Factory.StartNew(() => { LockSlimIntAdd(); });  61             }  62             Task.WaitAll(taskList);  63             Console.WriteLine($"{GetFormat("LockSlimIntAdd")}, 總耗時:{stopwatch.ElapsedMilliseconds}毫秒, 校驗值:{lockSlimInt}");  64  65             // SemaphoreIntAdd  66             taskList = new Task[loopTimes];  67             stopwatch.Restart();  68  69             for (var i = 0; i < loopTimes; i++)  70             {  71                 taskList[i] = Task.Factory.StartNew(() => { SemaphoreIntAdd(); });  72             }  73             Task.WaitAll(taskList);  74             Console.WriteLine($"{GetFormat("SemaphoreIntAdd")}, 總耗時:{stopwatch.ElapsedMilliseconds}毫秒, 校驗值:{semaphoreInt}");  75  76  77             // AutoResetEventIntAdd  78             taskList = new Task[loopTimes];  79             stopwatch.Restart();  80  81             for (var i = 0; i < loopTimes; i++)  82             {  83                 taskList[i] = Task.Factory.StartNew(() => { AutoResetEventIntAdd(); });  84             }  85             Task.WaitAll(taskList);  86             Console.WriteLine($"{GetFormat("AutoResetEventIntAdd")}, 總耗時:{stopwatch.ElapsedMilliseconds}毫秒, 校驗值:{autoResetEventInt}");  87  88             // NoLockIntAdd  89             taskList = new Task[loopTimes];  90             stopwatch.Restart();  91  92             for (var i = 0; i < loopTimes; i++)  93             {  94                 taskList[i] = Task.Factory.StartNew(() => { NoLockIntAdd(); });  95             }  96             Task.WaitAll(taskList);  97             Console.WriteLine($"{GetFormat("NoLockIntAdd")}, 總耗時:{stopwatch.ElapsedMilliseconds}毫秒, 校驗值:{noLockInt}");  98             Console.WriteLine();  99         }

View Code

2、執行緒:10

3、執行緒:50

三、總結

 

1)在各種測試中,不加鎖肯定是最快的,所以盡量避免資源競爭導致加鎖運行

2)在多執行緒中Interlocked.CompareExchange始終表現出優越的性能,排在第二位

3)第三位lock,臨界區也表現出很好的性能,所以在別人說lock性能低的時候請反駁他

4)第四位是原子性變數(Atomic)操作,不過目前只支援變數的自增自減,適用性不強

5)第五位讀寫鎖(ReaderWriterLockSlim)表現也還可以,並且支援無所讀,實用性還是比較好的

6)剩下的訊號量、事件、互斥量,這三種性能最差,當然他們有各自的適用範圍,只是在處理資源競爭這方面表現不好

 

over,就這樣吧,睡覺。。。