C# 线程(四):生产者和消费者

Fromhttp://kb.cnblogs.com/page/42530/

前面说过,每个线程都有自己的资源,但是代码区是共享的,即每个线程都可以执行相同的函数。这可能带来的问题就是几个线程同时执行一个函数,导致数据的混乱,产生不可预料的结果,因此我们必须避免这种情况的发生。

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(-50, 100));
        }
    }
    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

事实上,这个简单的例子已经帮助我们解决了多线程应用程序中可能出现的大问题,只要领悟了解决线程间冲突的基本方法,很容易把它应用到比较复杂的程序中去。

-------------->>> 

C# 线程(四):生产者和消费者

时间: 2024-10-08 22:15:15

C# 线程(四):生产者和消费者的相关文章

实验四生产者和消费者

实验四生产者和消费者 一.实验目的 1.掌握临界区的概念及临界区的设计原则: 2.掌握信号量的概念.PV操作的含义以及应用PV操作实现进程的同步与互斥: 3.分析进程争用资源的现象,学习解决进程互斥的方法. 二.实验内容和要求 分析进程的同步与互斥现象,编程实现经典的进程同步问题——生产者消费者问题的模拟 生产者--消费者问题表述: 有一环形缓冲池,包含n个缓冲区(0~n-1). 有两类进程:一组生产者进程和一组消费者进程,生产者进程向空的缓冲区中放产品,消费者进程从满的缓冲区中取走产品. 所有

Java---20---多线程:生产者和消费者2(JDK1.5升级版)

此篇文章写的是JDK1.5升级版的生产者和消费者. Java 升级之后的解决方法: Lock java.util.concurrent.locks 接口 Lock 所有已知实现类: ReentrantLock,ReentrantReadWriteLock.ReadLock,ReentrantReadWriteLock.WriteLock public interface Lock Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作.此实现允许更灵活的结构,可

Java---19---多线程:生产者和消费者

生产者和消费者问题是多线程通信的经典问题.这类问题描述了这样一种情况:假设有一个仓库,用来存储产品,有生产者负责生产产品,有消费者负责消费.生产者生产的产品存放在仓库之中,消费者从仓库之中取出产品.显然这是一个同步问题,生产者和消费者共享同一资源,并且生产者和消费者之间彼此依赖,互为条件向前推进.那么,该如何编写代码来实现呢? class Resource { private String name; private String sex; private boolean flag = fals

生产者、消费者模型

转载地址:http://blog.csdn.net/snow_5288/article/details/72794306 一.概念引入 日常生活中,每当我们缺少某些生活用品时,我们都会去超市进行购买,那么,你有没有想过,你是以什么身份去的超市呢?相信大部分人都会说自己是消费者,确实如此,那么既然我们是消费者,又是谁替我们生产各种各样的商品呢?当然是超市的各大供货商,自然而然地也就成了我们的生产者.如此一来,生产者有了,消费者也有了,那么将二者联系起来的超市又该作何理解呢?诚然,它本身是作为一座交

Java多线程--生产者与消费者问题

说明 Java中,线程之间的通信主要是由java.lang.Object类提供的wait.notify和notifyAll这3个方法来完成: ①对象的wait方法被调用后,线程进入对象的等待队列中,并释放对象锁,其它线程可以竞争使用此对象锁:sleep方法使得一个线程进入睡眠状态,但是线程所占有的资源并没有释放. ②当对象的notify方法被调用,该方法会从对象的等待队列中随机取出一个线程来唤醒:notifyAll是唤醒等待队列中所有线程,这些线程会与其它正在执行的线程共同竞争对象锁. ③wai

JAVA-初步认识-第十四章-线程间通信-多生产者多消费者问题-JDK1.5解决办法

一. 在1.5版本中,将原先的形式进行了改变,但是功能并没有任何变化,那么这么做的原因是什么? 以前,我们一个锁上只有一组监视器,这组监视器既监视着生产者,又监视着消费者.这组监视器能将生产者和消费者全都wait,也能将生产者和消费者全都唤醒.或者notify也行,它也能将其中一条线程唤醒,而其中一条不能确定是谁,有可能是本方,也可能是对方. 现在我们的线程进行了分类,一组负责生产,一组负责消费.我们希望生产者能够唤醒消费者,消费者唤醒生产者.如果搞两个监视器,一组监视生产者,一组监视消费者,这

生产者与消费者模式(线程的同步与互斥)

死锁产生的四个条件: 1.互斥使用(资源独占) 一个资源每次只能给一个进程使用 .2.不可强占(不可剥夺) 资源申请者不能强行的从资源占有者手中夺取资源,资源只能由占有者自愿释放 .3.请求和保持(部分分配,占有申请) 一个进程在申请新的资源的同时保持对原有资源的占有(只有这样才是动态申请,动态分配) .4.循环等待 存在一个进程等待队列 {P1 , P2 , - , Pn}, 其中P1等待P2占有的资源,P2等待P3占有的资源,-,Pn等待P1占有的资源,形成一个进程等待环路 生产者:生产数据

实验四、生产者和消费者

一.    实验目的 1.掌握临界区的概念及临界区的设计原则: 2.掌握信号量的概念.PV操作的含义以及应用PV操作实现进程的同步与互斥: 3.分析进程争用资源的现象,学习解决进程互斥的方法. 二.    实验内容及要求 分析进程的同步与互斥现象,编程实现经典的进程同步问题——生产者消费者问题的模拟 生产者--消费者问题表述: 有一环形缓冲池,包含n个缓冲区(0~n-1). 有两类进程:一组生产者进程和一组消费者进程,生产者进程向空的缓冲区中放产品,消费者进程从满的缓冲区中取走产品,所有进程必须

Java线程学习整理--4---一个简单的生产者、消费者模型

 1.简单的小例子: 下面这个例子主要观察的是: 一个对象的wait()和notify()使用情况! 当一个对象调用了wait(),那么当前掌握该对象锁标记的线程,就会让出CPU的使用权,转而进入该对象的等待池中等待唤醒,这里说明一下,每一个对象都有一个独立的等待池和锁池! 等待池:上述的wait()后的线程会进入等待池中,处于下图线程声明周期(简单示意图) 中的这个状态,等待池中的线程任然具有对象的锁标记,但是处于休眠状态,不是可运行状态! 当该对象调用notify方法之后,就会在等待池中系统