1.線程通信
起因:有時,當某一個線程進入同步方法之后,共享變量並不滿足它所需要的狀態,該線程需要等待其它線程將共享變量改為它所需要的狀態后才能往下執行。由於此時其它線程無法進入臨界區,所以就需要該線程放棄監視器,並返回到排隊狀態等待其它線程交回監視器。“生產者與消費者”問題就是這一類典型的問題,設計程序時必須解決:生產者比消費者快時,消費者會漏掉一些數據沒有取到的問題;消費者比生產者快時,消費者取相同數據的問題。
(1)使用Monitor實現線程通信
示例:模擬吃蘋果。爸爸媽媽不斷往盤子放蘋果,老大老二,老三不斷從盤子取蘋果吃。五個線程需要同步執行,並且要相互協調。爸爸和媽媽放蘋果的時候,盤子里要有地方,而且兩個人不能同時放。三個孩子取蘋果的時候盤子里要有蘋果,而且不能同時取。三個好孩子因為吃蘋果速度不同,取蘋果的頻率也不一樣,又因大小不一致,有不同的優先級。
理解:本問題類似於“生產者與消費者”問題,因此定義一個生產者線程類和一個消費者線程類。盤子是一個中間類,包含了共享數據區和放蘋果,取蘋果的方法。為了避免沖突,放蘋果和取蘋果兩個方法使用了同步控制機制。
class Program { static void Main(string[] args) { EatApple eatApple = new EatApple(); } } class EatApple { public EatApple() { //創建5個線程 Thread t1, t2, t3, t4, t5; Dish d = new Dish(this, 20); //媽媽線程 Productor mather = new Productor("媽媽", d); //爸爸線程 Productor father = new Productor("爸爸", d); //老大線程 Consumer one = new Consumer("老大", d, 1000); //老二線程 Consumer two = new Consumer("老二", d, 2000); //老三線程 Consumer three = new Consumer("老三", d, 3000); t1 = new Thread(mather.Run); t2 = new Thread(father.Run); t3 = new Thread(one.Run); t4 = new Thread(two.Run); t5 = new Thread(three.Run); //設置優先級 t1.Priority = ThreadPriority.Highest; t2.Priority = ThreadPriority.Normal; t3.Priority = ThreadPriority.BelowNormal; t4.Priority = ThreadPriority.Normal; t5.Priority = ThreadPriority.Highest; t1.Start(); t2.Start(); t3.Start(); t4.Start(); t5.Start(); Console.ReadKey(); } } /// <summary> /// 盤子類 /// </summary> class Dish { int f = 4;//一個盤子最多放4個蘋果 EatApple eat; int num = 0;//可放的蘋果數。 int n = 0; public Dish(EatApple eat, int num) { this.eat = eat; this.num = num; } public void Put(string name) { lock (this) { while (f == 0) { Console.WriteLine($"{name}等待放蘋果"); //釋放當前線程對象的鎖,讓其他線程訪問。 Monitor.Wait(this); } Random random = new Random(); //random.Next(f):獲取在0到f之間的任意一個值。 int x = (int)random.Next(f) + 1; f -= x; n += x; Console.WriteLine($"{name}放{x}個蘋果"); //現在盤子里有蘋果了,所以當前線程准備釋放鎖,向所有等待線程通知此消息。可以來拿蘋果了 Monitor.PulseAll(this); if (n >= num) { //如果放的個數已經大於盤子上限,停止線程。 Thread.CurrentThread.Abort(); } } } /// <summary> /// 獲取蘋果 /// </summary> /// <param name="name"></param> public void Get(string name) { //不管是那蘋果還是取蘋果,一次只能一個人, lock (this) { while (f == 4) { try { Console.WriteLine($"{name}等待取蘋果"); Monitor.Wait(this); } catch (ThreadInterruptedException e) { } } f += 1; Console.WriteLine($"{name}取蘋果吃"); Monitor.PulseAll(this); } } } /// <summary> /// 生產者類 /// </summary> class Productor { //盤子 private Dish dish; private string name; public Productor(string name, Dish dish) { this.dish = dish; this.name = name; } public void Run() { while(true) { dish.Put(name); Thread.Sleep(100); } } } /// <summary> /// 消費者類 /// </summary> class Consumer { //盤子 private Dish dish; private string name; private int time;//吃蘋果所用的時間 public Consumer(string name, Dish dish, int time) { this.dish = dish; this.name = name; this.time = time; } public void Run() { while (true) { dish.Get(name); Thread.Sleep(time); } } }
結果:
老二等待取蘋果
老大等待取蘋果
老三等待取蘋果
爸爸放3個蘋果
老二取蘋果吃
老大取蘋果吃
老三取蘋果吃
媽媽放3個蘋果
爸爸放1個蘋果
媽媽等待放蘋果
爸爸等待放蘋果
老大取蘋果吃
媽媽放1個蘋果
爸爸等待放蘋果
.......
(2)使用AutoResetEvent類和MaunalResetEvent類進行線程通信
AutoResetEvent允許線程通過發信號相互通信。線程通過調用AutoResetEvent上的WaitOne()來等待信號。如果AutoResetEvent處於無信號狀態,則當前線程阻塞,等待有信號才能繼續運行。調用Set可終止阻塞並釋放等待線程。AutoResetEvent將保持有信號狀態,直到一個正在等待的線程被釋放,然后自動返回無信號狀態,如果沒有任何線程等待,那么將一直保持有信號狀態。
MaunalResetEvent和AutoResetEvent的使用基本類似。不同的是AutoResetEvent在釋放某個線程之后會自動將信號轉換成無信號狀態(因為釋放線程之后,共享資源將被占用,自動轉為無信號狀態告訴其他等待線程還不能進入)。但是MaunalResetEvent要調用Reset()方法來設置成無信號狀態。
示例:五位哲學家坐在餐桌前,他們在思考並在感到飢餓時就吃東西。每兩位哲學家之間只有一根筷子,為了吃東西,一個哲學家必須要用2根筷子。如果每位哲學家拿起右筷子,然后等着拿左筷子,問題就產生了,在這種情況下,就會發生死鎖。當哲學家放下筷子時,要通知其他等待拿筷子的哲學家。
注意:為筷子單獨創建一個類,筷子是共享資源,由一個信號量AutoResetEvent來指明是否可用。由於任何時候只允許一位哲學家能拿起一根特定的筷子,所以拿筷子的方法需要被同步。當吃完時候通過putdown()來放下筷子。哲學家思考時通過think()放下筷子。
class Dining { //一共5個筷子,5位哲學家 static ChopStick[] cp = new ChopStick[5]; static Philosopher[] ph = new Philosopher[5]; static void Main(string[] args) { for (int i = 0; i < 5; i++) { cp[i] = new ChopStick(i); } for (int i = 0; i < 5; i++) { ph[i] = new Philosopher("哲學家"+i,cp[i],cp[(i+1)%5]); } for (int i = 0; i < 5; i++) { Thread thread = new Thread(ph[i].Run); thread.Start(); } } } /// <summary> /// 抽象出筷子類 /// 筷子為共享資源 /// </summary> class ChopStick { AutoResetEvent eventX; int n; public ChopStick(int n) { this.n = n; //設置一個初始狀態true。表明現在共享資源可用,是有信號狀態。 eventX = new AutoResetEvent(true); } /// <summary> /// 拿起筷子方法 /// </summary> /// <param name="name">哲學家</param> public void TakeUp(string name) { //每個筷子只能同時由一個人使用 lock(this) { Console.WriteLine($"{name}等待拿起筷子"); //當前線程等待拿起筷子,在獲取到信號之前是不能往下執行的。 eventX.WaitOne(); Console.WriteLine($"{name}拿起第{n}根筷子"); } } /// <summary> /// 放下筷子 /// </summary> public void PutDown(string name) { //釋放信號,使得等待線程可繼續往下執行。 eventX.Set(); Console.WriteLine($"{name}放下第{n}根筷子"); } } /// <summary> /// 哲學家類 /// </summary> class Philosopher { //左右筷子 ChopStick left, right; string name; public Philosopher(string name,ChopStick left,ChopStick right) { this.left = left; this.right = right; this.name = name; } public void Think() { left.PutDown(name); right.PutDown(name); Console.WriteLine($"{name}在思考"); } public void Eat() { left.TakeUp(name); right.TakeUp(name); Console.WriteLine($"{name}在吃飯"); } public void Run() { while(true) { Eat(); Thread.Sleep(1000); Think(); Thread.Sleep(1000); } } }
結果
哲學家0等待拿起筷子
哲學家2等待拿起筷子
哲學家4等待拿起筷子
哲學家3等待拿起筷子
哲學家3拿起第3根筷子
哲學家0拿起第0根筷子
哲學家2拿起第2根筷子
哲學家2等待拿起筷子
哲學家1等待拿起筷子
哲學家4拿起第4根筷子
哲學家4等待拿起筷子
哲學家1拿起第1根筷子
哲學家1等待拿起筷子
哲學家3等待拿起筷子
哲學家0等待拿起筷子
區別:https://www.cnblogs.com/jicheng/articles/5998244.html
https://www.cnblogs.com/gudi/p/6233977.html 前台線程和后台線程可以到這里看。