一、前言
線程同步其實很簡單,但是往往被老師教的很複雜。這是之前上課受的傷。腦袋瓜當人人家的跑馬場,被蹂躏一番,最後老師留下的是先入為主的錯誤,以至于後面不停的幹擾我的了解,糾起錯來,真是不知道浪費了多少精力。
二、什麼是線程同步
一直想要找一個良好的方式來表達什麼是線程同步。
先看一個模拟線程同步的圖:
假如這個盒子一次隻能放一個東西,并且接力賽又要保持順暢,該是怎樣的情景?
首先對于Reader來說,取貨的時候,箱子必須有貨,如果沒有貨,要在旁邊等候;
其次對于Writer來說,存貨的時候,箱子必須為空,如果不為空,也要在旁邊等候;
兩個人要步調一緻,并且配合默契,才能順利的搬運東西。反過來,如果Reader執行了好幾次,Writer才執行一次,或者Write執行了好幾次,Reader才執行一次,最後都不能很好的保持步調的一緻。
把兩個人看成是兩個線程,這時候線程要同步,也必須要滿足上面的要求。兩個線程要協同一緻的工作,才能完成一項任務。
三、代碼示範:
public class ThreadSyn
{
//緩存區,假設一次隻能緩存一個字元
private static char buffer;
//線程1:寫操作
public Thread thread1 = new Thread(()=>
{
string str = "橫看成嶺側成峰,遠近高低各不同。不識廬山真面目,隻緣身在此山中。";
for (int i = 0; i < 32; i++)
{
buffer = str[i];
Thread.Sleep(26);
}
});
//線程2:讀操作
public Thread thread2 = new Thread(() =>
{
for (int i = 0; i < 32; i++)
{
char ch = buffer;
Console.WriteLine(ch);
Thread.Sleep(36);
}
});
}
public class Program
{
static void Main(string[] args)
{
ThreadSyn threadSyn=new ThreadSyn();
threadSyn.thread1.Start();
threadSyn.thread2.Start();
Console.Read();
}
}
運作效果圖:
四、原因和方案:
此時線程還是沒有協同工作。因為如果寫一個,讀一個,再寫一個,再讀一個,那麼這首詩應該是一首完整的顯示。但是效果圖的詩句卻是紊亂的。
如何才能解決真正的同步,.net為我們提供了一系列的同步類。包括:互鎖(Interlocked),管程(Monitor)和互斥體(Mutex).
4.1下面用互鎖來解決上面的問題。
public class ThreadSyn
{
//緩存區
private static char _buffer;
//标示盒子,即緩沖區使用的空間,盒子初始化為0
private static long _box = 0;
//線程1:寫操作
public Thread thread1 = new Thread(()=>
{
string str = "橫看成嶺側成峰,遠近高低各不同。不識廬山真面目,隻緣身在此山中。";
for (int i = 0; i < 32; i++)
{
//寫入之前檢查緩沖區
//如果緩沖區已滿,就進行等待,直到緩沖區的資料被程序讀取為止
while(Interlocked.Read(ref _box) == 1)
{
Thread.Sleep(10);
}
//向緩沖區寫資料
_buffer = str[i];
//寫完資料,标記緩沖區已滿
Interlocked.Increment(ref _box);
}
});
//線程2:讀操作
public Thread thread2 = new Thread(() =>
{
for (int j = 0; j < 32; j++)
{
//寫入之前檢查緩沖區
//如果緩沖區為空,就進行等待,直到緩沖區的資料被程序填充為止
while(Interlocked.Read(ref _box) == 0)
{
Thread.Sleep(10);
}
//向緩沖區讀資料
char ch = _buffer;
Console.Write(ch);
//讀完資料,标記緩沖區已空
Interlocked.Decrement(ref _box);
}
});
}
運作效果圖:
InterLocked提供了單個指令的操作,是以他提供了性能非常高的同步。
4.2用Monitor來解決問題
Monitor的原理是這樣的:先執行的線程,獨占鎖,進入臨界區,執行臨界區資源代碼。其他線程,隻能在集中在臨界資源上等待被叫喚。當獨占鎖推出資源區,也可以繼續讓自己等待,等待下一次被叫喚。
//緩存區
private static char _buffer;
//用于同步的對象
private static object _objForLock = new object();
//線程1:寫操作
public Thread thread1 = new Thread(() =>
{
string str = "橫看成嶺側成峰,遠近高低各不同。不識廬山真面目,隻緣身在此山中。";
for (int i = 0; i < 32; i++)
{
try
{
//進入臨界區,擷取獨占鎖
Monitor.Enter(_objForLock);
//向緩沖區寫資料
_buffer = str[i];
//寫完後,喚醒在臨界資源上睡眠的線程
Monitor.Pulse(_objForLock);
//讓目前線程睡眠在臨界資源上
Monitor.Wait(_objForLock);
//整個流程有點像輪班吃飯,
//第一個人先去吃飯,第二個在值班等待,第一個吃完了,喚醒第二個吃飯,自己則在等待下一次吃飯。
}
catch (ThreadInterruptedException ex)
{
Console.WriteLine("線程被中斷……");
}
finally
{
//退出臨界區
Monitor.Exit(_objForLock);
}
}
});
//線程2:讀操作
public Thread thread2 = new Thread(() =>
{
for (int j = 0; j < 32; j++)
{
try
{
//進入臨界區,擷取獨占鎖
Monitor.Enter(_objForLock);
//向緩沖區讀資料
char ch = _buffer;
Console.Write(ch);
//寫完後,喚醒在臨界資源上睡眠的線程
Monitor.Pulse(_objForLock);
//讓目前線程睡眠在臨界資源上
Monitor.Wait(_objForLock);
}
catch (ThreadInterruptedException ex)
{
Console.WriteLine("線程被中斷……");
}
finally
{
//退出臨界區
Monitor.Exit(_objForLock);
}
}
});
不同的是Monitor隻能鎖定引用類型的對象,值類型會被裝箱,等于生成另外一個對象,不能達到同步。為了保證推出臨界區資源得到釋放,使用了finally。為了友善使用,C#專門使用了lock語句。
是以我們可以完全更簡潔的重寫上面的try{}finally{}中的關鍵代碼,如下所示:
lock (_objForLock)
{
//進入臨界區,擷取獨占鎖
Monitor.Enter(_objForLock);
//向緩沖區寫資料
_buffer = str[i];
//寫完後,喚醒在臨界資源上睡眠的線程
Monitor.Pulse(_objForLock);
//讓目前線程睡眠在臨界資源上
Monitor.Wait(_objForLock);
}
獨占鎖注意:
因為獨占鎖,其他線程就不能再通路,隻有Lock結束後,其他線程才可以通路,這保證了通路的正确性。但是,如果有多個線程對同一個資源進行寫操作,在獨占鎖解開前,其他線程隻能被臨時暫停,這使得程式的效率大打折扣。是以應該慎用鎖,隻有必要時才使用。