C# 線程同步 生産者 消費者問題
現在有五個勞工在果園摘水果,一次隻能摘一個,摘下的水果放入一個框中,這種框最多隻能裝50個橘子,一共有兩個這樣的框。當一個勞工裝框時,其他勞工不能在使用這個框。當兩個框都裝滿了,勞工隻有等框裡有剩餘位置後,才能在摘果子。然後,有四個小孩來框裡拿橘子,一次最多拿一個,并且當一個小孩在框前拿橘子時,其他小孩隻能到另一個框拿橘子,如果兩個框前都有小孩拿橘子,那麼剩餘要拿橘子的小孩隻能等待。(這個題目是我自己編的,可能不是很準确)
現在要設計一個程式模拟這樣一個過程。
分析:框是互斥資源,每次放橘子前 得判斷有沒有空閑得框,有就占住加鎖,然後裡面執行放橘子得方法。放完之後再解鎖。框可以用隊清單示。
勞工和小孩可以用Task模拟。
這裡需要兩種鎖,一種是放橘子得時候得一把Monitor鎖,一種是當沒有空閑得框後,加的AutoResetEvent鎖。
當使用兩把鎖得時候,需要特别小心,稍不注意都會引發死鎖。
Monitor鎖再使用得時候,得用引用變量作為加鎖得對象,不要用字元串和值變量。雖然再用值變量時,編譯器不會報錯,但是運作時,Enter會裝箱,把值變量變為引用變量,但是再Exit時,依然是個值變量,這樣Enter和Exit的鎖變量就不是同一個變量,造成找不到鎖的情況,就會抛出異常。
另外使用Monitor枷鎖時,應該使用 try{}finally{}語句塊,保證總是會被解鎖,否則遇到異常,不執行解鎖語句,就死鎖了。
其實lock語句塊就是Monitor的簡便方法,内部使用的還是Monitor。
對于AutoResetEvent而言,可以暫停和喚醒線程,再不同線程可以互相喚醒和阻塞。這樣就非常的靈活。其實推薦使用ManualResetEvent,因為比起AutoResetEvent,可以喚起多個線程,如果說小孩一次拿多個橘子,這種方式就比AutoResetEvent有優勢,因AutoResetEvent隻喚醒一個線程。
線程的同步還有其他方法,比如再數值上同步 有InterLock,其他的如信号量(Sema'phore)同步,CountDownEvent。
同步的應用,還可以是用程序間同步的方法,實作在一台主機上,每次隻能啟動一個相同的應用程式,這時可以使用Mutex。
避免資源線上程同步中互斥,還可以用 線程本地存儲技術,ThreadLocal,例子:

詳見:《C#本質論》第三版,第19章
下面直接看代碼:
internal class Program
{
//最多容納50個橘子每個框
static readonly int MAX = 50;
//兩個框
static List<ConcurrentQueue<Orange>> Queues =
new List<ConcurrentQueue<Orange>>();
//記錄空閑的框
static List<int> QidxBags = new List<int>();
static int MaxO = 1000; //最多摘1000個橘子
static readonly object Sync = new object();
static readonly object Sync2 = new object();
//比起AutoResetEvent,可以喚起多個線程,如果說小孩一次拿多個橘子,而不是一個,
//這種方式比AutoResetEvent有優勢,因為AutoResetEvent隻喚醒一個線程。
static ManualResetEvent MResetEvent = new ManualResetEvent(false);
static void Main(string[] args)
{
Queues.Add(new ConcurrentQueue<Orange>());
Queues.Add(new ConcurrentQueue<Orange>());
for (int i = 0; i < Queues.Count; i++)
{
QidxBags.Add(i);
}
TaskProduceAndComsummer();
Console.ReadKey();
}
static int GetQueuesIdx()
{
int idx = -1;
int count = QidxBags.Count;
if (count > 0)
{
return count;
}
return idx;
}
static bool IsEmpty()
{
foreach (var item in Queues)
{
if (item.Count >0)
{
return false;
}
}
return true;
}
static bool IsFull()
{
foreach (var item in Queues)
{
if (item.Count < MAX)
{
return false;
}
}
return true;
}
static void TaskProduceAndComsummer()
{
for (int i = 0; i < 5; i++)
{
string name = "勞工_" + (i + 1);
Task t = new Task(Produce, (object)(name));
t.Start();
}
for (int i = 0; i < 3; i++)
{
string name = "小孩_" + (i + 1);
Task t = new Task(Consumer, (object)(name));
t.Start();
}
}
static void Produce(object name)
{
while (true&&MaxO>0)
{
int count = -1;
int iPos = -1;
lock (Sync2)
{
count = GetQueuesIdx();
}
if (count > 0&&!IsFull())
{
bool refTaken = false;
Monitor.Enter(Sync, ref refTaken);
bool isPut = false;
try
{
for (int i = 0; i < count; i++)
{
iPos = QidxBags[i];
var q = Queues[iPos];
if (q.Count < MAX)
{
QidxBags.Remove(iPos);
q.Enqueue(Orange.GetOrange());
MaxO -= 1;
Console.WriteLine(name + ":+摘了一個橘子,放入框【" + iPos + "】中");
Console.WriteLine("框一數量:{0},框二數量{1}", Queues[0].Count, Queues[1].Count);
isPut = true;
//喚醒小孩線程
MResetEvent.Set();
break;
}
}
}
finally
{
if (refTaken)
{
if (iPos > -1)
{
QidxBags.Add(iPos);
}
Monitor.Exit(Sync);
if (!isPut)
{
Console.WriteLine("滿了");
}
}
}
}
else
{
MResetEvent.WaitOne();
}
}
}
static void Consumer(object name)
{
while (true)
{
int count = GetQueuesIdx();
int iPos = -1;
if (count > 0&&!IsEmpty())
{
bool refTaken = false;
bool isPut = false;
Monitor.Enter(Sync, ref refTaken);
try
{
for (int i = 0; i < count; i++)
{
iPos = QidxBags[i];
var q = Queues[iPos];
if (q.Count >0)
{
QidxBags.Remove(iPos);
Orange o = null;
q.TryDequeue(out o);
Console.WriteLine(name + ":+拿了一個橘子,從框【" + iPos + "】中");
Console.WriteLine("框一數量:{0},框二數量{1}", Queues[0].Count, Queues[1].Count);
isPut = true;
//框有容量了,可以放了,是以喚醒被阻塞得勞工線程
MResetEvent.Set();
break;
}
}
}
finally
{
if (refTaken)
{
if (iPos > -1)
{
QidxBags.Add(iPos);
}
Monitor.Exit(Sync);
if (!isPut)
{
Console.WriteLine("都空了");
}
}
}
}
else
{
MResetEvent.WaitOne();//阻塞
}
}
}
}
public class Orange
{
public static Orange GetOrange()
{
Random rand = new Random();
int t = rand.Next(10, 20);
Thread.Sleep(t);
return new Orange();
}
}
部分結果: