前言
一般情況下,只要涉及到多執行緒程式設計,程式的複雜性就會顯著上升,效能顯著下降,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 }
2、執行緒:10
3、執行緒:50
三、總結
1)在各種測試中,不加鎖肯定是最快的,所以儘量避免資源競爭導致加鎖執行
2)在多執行緒中Interlocked.CompareExchange始終表現出優越的效能,排在第二位
3)第三位lock,臨界區也表現出很好的效能,所以在別人說lock效能低的時候請反駁他
4)第四位是原子性變數(Atomic)操作,不過目前只支援變數的自增自減,適用性不強
5)第五位讀寫鎖(ReaderWriterLockSlim)表現也還可以,並且支援無所讀,實用性還是比較好的
6)剩下的訊號量、事件、互斥量,這三種效能最差,當然他們有各自的適用範圍,只是在處理資源競爭這方面表現不好
over,就這樣吧,睡覺。。。