C#多線程學習(三) 生產者和消費者

前面說過,每一個線程都有本身的資源,可是代碼區是共享的,即每一個線程均可以執行相同的函數。這可能帶來的問題就是幾個線程同時執行一個函數,致使數據的混亂,產生不可預料的結果,所以咱們必須避免這種狀況的發生。
 
C#提供了一個關鍵字lock,它能夠把一段代碼定義爲互斥段(critical section),互斥段在一個時刻內只容許一個線程進入執行,而其餘線程必須等待。在C#中,關鍵字lock定義以下:
lock(expression) statement_block 
 
expression表明你但願跟蹤的對象,一般是對象引用。
    若是你想保護一個類的實例,通常地,你可使用this;
    若是你想保護一個靜態變量(如互斥代碼段在一個靜態方法內部),通常使用類名就能夠了。
而statement_block就是互斥段的代碼,這段代碼在一個時刻內只可能被一個線程執行。
 
下面是一個使用lock關鍵字的典型例子,在註釋裏說明了lock關鍵字的用法和用途。
示例以下:
using System;
using System.Threading;
 
namespace ThreadSimple
{
    internal class Account 
    {
        int balance;
        Random r = new Random();
        
        internal Account(int initial) 
        {
            balance = initial;
        } 
 
        internal int Withdraw(int amount) 
        {
            if (balance < 0)
            {
                //若是balance小於0則拋出異常
                throw new Exception("Negative Balance");
            }
            //下面的代碼保證在當前線程修改balance的值完成以前
            //不會有其餘線程也執行這段代碼來修改balance的值
            //所以,balance的值是不可能小於0 的
            lock (this)
            {
                Console.WriteLine("Current Thread:"+Thread.CurrentThread.Name);
                //若是沒有lock關鍵字的保護,那麼可能在執行完if的條件判斷以後
                //另一個線程卻執行了balance=balance-amount修改了balance的值
                //而這個修改對這個線程是不可見的,因此可能致使這時if的條件已經不成立了
                //可是,這個線程卻繼續執行balance=balance-amount,因此致使balance可能小於0
                if (balance >= amount) 
                {
                    Thread.Sleep(5);
                    balance = balance - amount;
                    return amount;
                } 
                else 
                {
                    return 0// transaction rejected
                  }
            }
        }
        internal void DoTransactions() 
        {
            for (int i = 0; i < 100; i++) 
            Withdraw(r.Next(-50100));
        }
    } 
 
    internal class Test 
    {
        static internal Thread[] threads = new Thread[10];
        public static void Main() 
        {
            Account acc = new Account (0);
            for (int i = 0; i < 10; i++) 
            {
                Thread t = new Thread(new ThreadStart(acc.DoTransactions));
                threads[i] = t;
            }
            for (int i = 0; i < 10; i++) 
                threads[i].Name=i.ToString();
            for (int i = 0; i < 10; i++) 
                threads[i].Start();
            Console.ReadLine();
        }
    }
}
 
 
Monitor 類鎖定一個對象
當多線程公用一個對象時,也會出現和公用代碼相似的問題,這種問題就不該該使用lock關鍵字了,這裏須要用到System.Threading中的一個類Monitor,咱們能夠稱之爲監視器,Monitor提供了使線程共享資源的方案。
Monitor類能夠鎖定一個對象,一個線程只有獲得這把鎖才能夠對該對象進行操做。對象鎖機制保證了在可能引發混亂的狀況下一個時刻只有一個線程能夠訪問這個對象。
Monitor必須和一個具體的對象相關聯,可是因爲它是一個靜態的類,因此不能使用它來定義對象,並且它的全部方法都是靜態的,不能使用對象來引用。下面代碼說明了使用Monitor鎖定一個對象的情形:
......
Queue oQueue=new Queue();
......
Monitor.Enter(oQueue);
......//如今oQueue對象只能被當前線程操縱了
Monitor.Exit(oQueue);//釋放鎖 
 
如上所示,當一個線程調用Monitor.Enter()方法鎖定一個對象時,這個對象就歸它全部了,其它線程想要訪問這個對象,只有等待它使用Monitor.Exit()方法釋放鎖。爲了保證線程最終都能釋放鎖,你能夠把Monitor.Exit()方法寫在try-catch-finally結構中的finally代碼塊裏。
對於任何一個被Monitor鎖定的對象,內存中都保存着與它相關的一些信息:
其一是如今持有鎖的線程的引用;
其二是一個預備隊列,隊列中保存了已經準備好獲取鎖的線程;
其三是一個等待隊列,隊列中保存着當前正在等待這個對象狀態改變的隊列的引用。
當擁有對象鎖的線程準備釋放鎖時,它使用Monitor.Pulse()方法通知等待隊列中的第一個線程,因而該線程被轉移到預備隊列中,當對象鎖被釋放時,在預備隊列中的線程能夠當即得到對象鎖。
 
下面是一個展現如何使用lock關鍵字和Monitor類來實現線程的同步和通信的例子,也是一個典型的生產者與消費者問題。
這個例程中,生產者線程和消費者線程是交替進行的,生產者寫入一個數,消費者當即讀取而且顯示(註釋中介紹了該程序的精要所在)。
用到的系統命名空間以下:
using System;
using System.Threading;
首先,定義一個被操做的對象的類Cell,在這個類裏,有兩個方法:ReadFromCell()和WriteToCell。消費者線程將調用ReadFromCell()讀取cellContents的內容而且顯示出來,生產者進程將調用WriteToCell()方法向cellContents寫入數據。
 
示例以下:
public class Cell
{
        int cellContents; // Cell對象裏邊的內容
        bool readerFlag = false// 狀態標誌,爲true時能夠讀取,爲false則正在寫入
        public int ReadFromCell( )
        {
            lock(this// Lock關鍵字保證了什麼,請你們看前面對lock的介紹
            {
                if (!readerFlag)//若是如今不可讀取
                { 
                    try
                    {
                        //等待WriteToCell方法中調用Monitor.Pulse()方法
                        Monitor.Wait(this);
                    }
                    catch (SynchronizationLockException e)
                    {
                        Console.WriteLine(e);
                    }
                    catch (ThreadInterruptedException e)
                    {
                        Console.WriteLine(e);
                    }
                }
                Console.WriteLine("Consume: {0}",cellContents);
                readerFlag = false;
                //重置readerFlag標誌,表示消費行爲已經完成
                Monitor.Pulse(this); 
                //通知WriteToCell()方法(該方法在另一個線程中執行,等待中)
            }
            return cellContents;
        }
    
        public void WriteToCell(int n)
        {
            lock(this)
            {
                if (readerFlag)
                {
                    try
                    {
                        Monitor.Wait(this);
                    }
                    catch (SynchronizationLockException e)
                    {
                            //當同步方法(指Monitor類除Enter以外的方法)在非同步的代碼區被調用
                        Console.WriteLine(e);
                    }
                    catch (ThreadInterruptedException e)
                    {
                            //當線程在等待狀態的時候停止 
                        Console.WriteLine(e);
                    }
                }
                cellContents = n;
                Console.WriteLine("Produce: {0}",cellContents);
                readerFlag = true
                Monitor.Pulse(this); 
                //通知另一個線程中正在等待的ReadFromCell()方法
            }
        }
}
 
下面定義生產者類 CellProd 和消費者類 CellCons ,它們都只有一個方法ThreadRun(),以便在Main()函數中提供給線程的ThreadStart代理對象,做爲線程的入口。
public class CellProd
{
    Cell cell; // 被操做的Cell對象
    int quantity = 1// 生產者生產次數,初始化爲1 
 
    public CellProd(Cell box, int request)
    {
        //構造函數
        cell = box; 
        quantity = request; 
    }
    public void ThreadRun( )
    {
        for(int looper=1; looper<=quantity; looper++)
        cell.WriteToCell(looper); //生產者向操做對象寫入信息
    }
}
 
public class CellCons
{
    Cell cell; 
    int quantity = 1
 
    public CellCons(Cell box, int request)
    {
                //構造函數
        cell = box; 
        quantity = request; 
    }
    public void ThreadRun( )
    {
        int valReturned;
        for(int looper=1; looper<=quantity; looper++)
        valReturned=cell.ReadFromCell( );//消費者從操做對象中讀取信息
    }
 
而後在下面這個類MonitorSample的Main()函數中,咱們要作的就是建立兩個線程分別做爲生產者和消費者,使用CellProd.ThreadRun()方法和CellCons.ThreadRun()方法對同一個Cell對象進行操做。
 
public class MonitorSample
{
    public static void Main(String[] args)
    {
        int result = 0//一個標誌位,若是是0表示程序沒有出錯,若是是1代表有錯誤發生
        Cell cell = new Cell( ); 
 
        //下面使用cell初始化CellProd和CellCons兩個類,生產和消費次數均爲20次
        CellProd prod = new CellProd(cell, 20); 
        CellCons cons = new CellCons(cell, 20); 
 
        Thread producer = new Thread(new ThreadStart(prod.ThreadRun));
        Thread consumer = new Thread(new ThreadStart(cons.ThreadRun));
        //生產者線程和消費者線程都已經被建立,可是沒有開始執行 
        try
        {
    producer.Start( );
    consumer.Start( ); 
 
    producer.Join( ); 
    consumer.Join( );
    Console.ReadLine();
        }
        catch (ThreadStateException e)
        {
    //當線程由於所處狀態的緣由而不能執行被請求的操做
    Console.WriteLine(e); 
    result = 1
        }
        catch (ThreadInterruptedException e)
        {
    //當線程在等待狀態的時候停止
    Console.WriteLine(e); 
    result = 1
        }
        //儘管Main()函數沒有返回值,但下面這條語句能夠向父進程返回執行結果
        Environment.ExitCode = result;
    }
}
 
在上面的例程中,同步是經過等待Monitor.Pulse()來完成的。首先生產者生產了一個值,而同一時刻消費者處於等待狀態,直到收到生產者的「脈衝(Pulse)」通知它生產已經完成,此後消費者進入消費狀態,而生產者開始等待消費者完成操做後將調用Monitor.Pulese()發出的「脈衝」。
它的執行結果很簡單:
Produce: 1
Consume: 1
Produce: 2
Consume: 2
Produce: 3
Consume: 3
...
...
Produce: 20
Consume: 20 
 
事實上,這個簡單的例子已經幫助咱們解決了多線程應用程序中可能出現的大問題,只要領悟瞭解決線程間衝突的基本方法,很容易把它應用到比較複雜的程序中去。
相關文章
相關標籤/搜索