SemaphoreSlim是一個用於同步和限制併發訪問的類,和它類似的還有Semaphore,只是SemaphoreSlim更加的輕量、高效、好用。今天說說它,以及如何使用,在什麼時候去使用,使用它將會帶來什麼優勢。
代碼的業務是:
在多線程下進行數據的統計工作,簡單點的說就是累加數據。
1.首先我們建立一個程序
代碼如下
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace ConsoleApp2 { class Program { static int a = 0; static async Task Main(string[] args) { Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); Task t1 = Task.Run(() => { A(); }); Task t2 = Task.Run(() => { B(); }); await Task.WhenAll(t1, t2); stopwatch.Stop(); Console.WriteLine("總時間:" + stopwatch.ElapsedMilliseconds); Console.WriteLine("總數:" + a); Console.ReadLine(); } private static void A() { for (int i = 0; i < 100_0000; i++) { a++; } } private static void B() { for (int i = 0; i < 100_0000; i++) { a++; } } } }
2.運行結果
此時需要多運行幾次,會發現,偶爾出現運行的結果不一樣,這就是今天的問題
3.分析結果
從結果看,明顯錯誤了,正確答案是:200 0000,但是第二次的結果是131 6465。我們的業務就是開啓2個線程,一個A方法,一個B方法,分別對a的數據進行累加計算。那麼爲什麼造成這樣的結果呢?
造成這樣的原因就是多線程的問題。
解決方法一:
1.使用lock
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace ConsoleApp2 { class Program { static int a = 0; static object o = new object(); static async Task Main(string[] args) { Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); Task t1 = Task.Run(() => { A(); }); Task t2 = Task.Run(() => { B(); }); await Task.WhenAll(t1, t2); stopwatch.Stop(); Console.WriteLine("總時間:" + stopwatch.ElapsedMilliseconds); Console.WriteLine("總數:" + a); Console.ReadLine(); } private static void A() { for (int i = 0; i < 100_0000; i++) { lock (o) { a++; } } } private static void B() { for (int i = 0; i < 100_0000; i++) { lock (o) { a++; } } } } }
2. lock的結果
當我們增加lock後,不管運行幾次,結果都是正確的。
解決方法二:
1.使用SemaphoreSlim
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace ConsoleApp2 { class Program { static int a = 0; static object o = new object(); static SemaphoreSlim semaphore = new SemaphoreSlim(1); //控制訪問線程的數量 static async Task Main(string[] args) { Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); Task t1 = Task.Run(() => { A(); }); Task t2 = Task.Run(() => { B(); }); await Task.WhenAll(t1, t2); stopwatch.Stop(); Console.WriteLine("總時間:" + stopwatch.ElapsedMilliseconds); Console.WriteLine("總數:" + a); Console.ReadLine(); } private static void A() { for (int i = 0; i < 100_0000; i++) { semaphore.Wait(); //lock (o) //{ a++; //} semaphore.Release(); } } private static void B() { for (int i = 0; i < 100_0000; i++) { semaphore.Wait(); //lock (o) //{ a++; //} semaphore.Release(); } } } }
2.SemaphoreSlim的效果
當我們增加SemaphoreSlim後,不管運行幾次,結果都是正確的。
4.我們對比方法一和方法二發現,他們的結果都是一樣的,但是lock似乎比SemaphoreSlim更加的高效,是的,lock解決此業務的確比SemaphoreSlim高效。但是lock能幹的事,SemaphoreSlim肯定能幹,SemaphoreSlim能幹的事,lock不一定能幹。
5.SemaphoreSlim的使用
SemaphoreSlim使用的範圍非常的廣,可以限制訪問資源的線程數,例如限制一個資源最多5個線程可以同時訪問
using System.Threading; class Program { static SemaphoreSlim semaphore = new SemaphoreSlim(5); // 允許最多5個線程同時訪問資源 static void Main() { for (int i = 0; i < 10; i++) { Task.Run(DoWork); } Console.ReadLine(); } static async Task DoWork() { await semaphore.WaitAsync(); // 等待許可 try { Console.WriteLine($"線程 {Thread.CurrentThread.ManagedThreadId} 開始工作"); await Task.Delay(1000); // 模擬耗時操作 Console.WriteLine($"線程 {Thread.CurrentThread.ManagedThreadId} 結束工作"); } finally { semaphore.Release(); // 釋放許可 } } }
此時DoWork()這個方法,最多同時只有5個線程訪問,當改成1個,就是按照順序進行了,和lock的使用是一樣的
6.總結
如果需要確保同一時間只有一個線程訪問某資源(此案例指的就是變量a),那麼可以使用Lock,也可以使用SemaphoreSlim;如果需要控制同時訪問資源的線程數量,並且需要更復雜的信號量操作,那麼可以使用SemaphoreSlim。總之,使用Lock還是SemaphoreSlim,都是根據具體業務而定。
拓展:
當我們在第1步,只需要增加一句話,不增加lock和SemaphoreSlim,依然可以使得計算的結果準確,那就是增加
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace ConsoleApp2 { class Program { static int a = 0; static async Task Main(string[] args) { Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); Task t1 = Task.Run(() => { A(); }); Task t2 = Task.Run(() => { B(); }); await Task.WhenAll(t1, t2); stopwatch.Stop(); Console.WriteLine("總時間:" + stopwatch.ElapsedMilliseconds); Console.WriteLine("總數:" + a); Console.ReadLine(); } private static void A() { for (int i = 0; i < 10_0000; i++) { a++; Console.WriteLine(a); } } private static void B() { for (int i = 0; i < 10_0000; i++) { a++; Console.WriteLine(a); } } } }
效果