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 
 
事实上,这个简单的例子已经帮助咱们解决了多线程应用程序中可能出现的大问题,只要领悟了解决线程间冲突的基本方法,很容易把它应用到比较复杂的程序中去。
相关文章
相关标签/搜索