上一篇文章主要帶領大家認識了線程,也了解到了線程的基本用法和狀態,接下來就讓我們一起學習下什么是線程同步。
線程中異常的處理
在線程中始終使用try/catch代碼塊是非常重要的,因為不可能在線程代碼之外來捕獲到異常。
可以閱讀下面的代碼,這塊是做的驗證,證明在線程之外捕獲異常是錯誤的選擇,應該在線程中時時刻刻都使用異常處理機制。
static void Main(string[] args)
{
Thread twoThread = new Thread(TwoMethod);
twoThread.Start();
twoThread.Join();
try
{
Thread oneThread = new Thread(OneMethod);
oneThread.Start();
}
catch (Exception ex)
{
Console.WriteLine("外部捕獲線程one的異常:"+ex.Message);
}
Console.ReadKey();
}
static void OneMethod()
{
Console.WriteLine("Start OneMethod");
Thread.Sleep(TimeSpan.FromSeconds(2));
throw new Exception("異常01");
}
static void TwoMethod()
{
try
{
Console.WriteLine("Start TwoMethod");
Thread.Sleep(TimeSpan.FromSeconds(1));
throw new Exception("異常02");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
下面圖片是輸出報錯的結果,可以看到OneThread的異常沒有被外部的try/catch捕獲到,導致直接在線程內部提示錯誤,導致程序崩潰。
看到這個情況,那么我們在以后使用線程的時候,就需要特別的注意,一定要在線程中進行異常的處理和捕獲,千萬別遺留任何未處理的異常,因為如果線程中有未被處理的異常會導致整個程序都會受到影響,可能導致整個軟件崩潰。
線程同步
在上一篇推文中,我們了解到了Lock加鎖的機制,它是可以保證將某個變量或者某個模塊鎖住,當出現多個線程同時訪問時鎖就會起作用,只允許一個線程訪問,其余的等待,其訪問完后其余的才可以進行訪問。但是這種機制有一定的局限性,在多核CPU設備中,讓其余線程等待是極大浪費資源的,而且這種解決辦法會導致死鎖的現象。
上面說的也就是所謂的競爭條件問題的解決方法,導致這個問題的原因是多線程的執行並沒有正確同步。當一個線程執行遞增和遞減操作時,其他線程需要依次等待。這種常見問題通常被稱為線程同步。這種問題出現在當線程中有共享資源或者對象時,才會進行線程同步,如果無共享對象,則無需進行線程同步。
當在線程中有共享資源時,可使用下面的兩種方式進行處理。
一、原子操作
來實現對共享資源的訪問。其實就是一個操作只占用一個量子的時間,一次就可以完成。也就是說只有當前操作完成后,其他線程才能執行其他操作。這樣就避免了使用鎖,排除了死鎖的情況。
原子操作就是使用C#系統自帶的Interlocked類來對線程不安全的對象進行處理,借助Interlocked類,無需鎖定任何對象即可獲取到正確的結果,Interlocked類提供Increment,Decrement和Add等基本數學操作的原子方法,從而可以幫助我們無需使用鎖🔒,避免出現各種死鎖問題。
/// <summary>
/// 線程不安全
/// </summary>
class Counter
{
private int _count;
public int Count { get { return _count; } }
public void Add()
{
_count++;
}
public void Delete()
{
_count--;
}
}
/// <summary>
/// 不加鎖 ,但線程是安全的。
/// 可避免在線程中出現死鎖
/// </summary>
class CounterNoLock
{
private int _count;
public int Count { get { return _count; } }
public void Add()
{
Interlocked.Increment(ref _count); //遞增
}
public void Delete()
{
Interlocked.Decrement(ref _count); //遞減
}
}
二、將等待的線程置於阻塞狀態
這個處理方法是在第一個原子操作無效切程序的邏輯更加復雜的情況下才使用的,用於協調線程。
當線程處理阻塞狀態時,只會占用盡可能少的CPU時間,這就意味着將引入至少一次所謂的上下文切換。
上下文切換:指操作系統的線程調度器,該調度器會保持等待的線程的狀態,並切換到另一個線程,依次恢復等待的線程狀態。雖然會消耗極大的資源,但是如果線程被掛起很長時間這么做是值得的。這種也就內核模式,因為只有操作系統的內核才能阻止線程使用CPU時間。
用戶模式: 如果線程只是等候一小會,那最好只是簡單的等待,而不用將線程切換到阻塞狀態。還有一種為混合模式,也就是先嘗試使用用戶模式,如果線程等候時間過長,則會切換到阻塞狀態以節省CPU資源。
下面的DEMO主要介紹SemaphoreSlim類,該類用於限制了同時訪問同一個資源的線程數量。
static void Main(string[] args)
{
for (int i = 1; i <=6; i++)
{
string threadName = "Thread " + i;
int secondsWait = 2;
var thread = new Thread(( )=>DataConnect(threadName,secondsWait));
thread.Start();
}
Console.ReadKey();
}
static SemaphoreSlim _semaphore = new SemaphoreSlim(4); //默認4個線程可同時訪問
static void DataConnect(string name,int seconds)
{
Console.WriteLine("wait 線程的名字:",name);
_semaphore.Wait();
Console.WriteLine("Connect 線程的名字:" + name);
Thread.Sleep(TimeSpan.FromSeconds(seconds));
_semaphore.Release();
}
上面的代碼利用SemaphoreSlim類,設置其構造函數為4,也就是其指定允許的並發線程數量。上面使用信號系統限制了訪問數據連接的並發數為4個,當有4個線程進行訪問時,其他兩個線程需要等待,知道之前線程中某一個完成工作並調用Relece方法來發出信號。
小寄語
人生短暫,我不想去追求自己看不見的,我只想抓住我能看的見的。
原創不易,給個關注。
我是阿輝,感謝您的閱讀,如果對你有幫助,麻煩點贊、轉發 謝謝。