C#多线程学习之(三)生产者和消费者用法分析

本文实例讲述了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(-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#程序设计有所帮助。

(0)

相关推荐

  • C#多线程学习之(一)多线程的相关概念分析

    本文详细分析了C#多线程学习之多线程的相关概念.分享给大家供大家参考.具体分析如下: 什么是进程? 当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源. 而一个进程又是由多个线程所组成的. 什么是线程? 线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针.程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数. 什么是多线程? 多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序

  • C#多线程之Thread中Thread.IsAlive属性用法分析

    本文实例讲述了C#多线程之Thread中Thread.IsAlive属性用法.分享给大家供大家参考.具体如下: Thread.IsAlive属性 ,表示该线程当前是否为可用状态 如果线程已经启动,并且当前没有任何异常的话,则是true,否则为false Start()后,线程不一定能马上启动起来,也许CPU正在忙其他的事情,但迟早是会启动起来的! Thread oThread = new Thread(new ThreadStart(Back.Start)); oThread.Start();

  • C#多线程之Thread中Thread.Join()函数用法分析

    本文实例讲述了C#多线程之Thread中Thread.Join()函数用法.分享给大家供大家参考.具体分析如下: Thread.Join()在MSDN中的解释:Blocks the calling thread until a thread terminates 当NewThread调用Join方法的时候,MainThread就被停止执行, 直到NewThread线程执行完毕. Thread oThread = new Thread(new ThreadStart(oAlpha.Beta));

  • C#多线程学习之(六)互斥对象用法实例

    本文实例讲述了C#多线程学习之互斥对象用法.分享给大家供大家参考.具体分析如下: 如何控制好多个线程相互之间的联系,不产生冲突和重复,这需要用到互斥对象,即:System.Threading 命名空间中的 Mutex 类. 我们可以把Mutex看作一个出租车,乘客看作线程.乘客首先等车,然后上车,最后下车.当一个乘客在 车上时,其他乘客就只有等他下车以后才可以上车.而线程与Mutex对象的关系也正是如此,线程使用Mutex.WaitOne()方法等待Mutex对 象被释放,如果它等待的Mutex

  • C#多线程学习之(四)使用线程池进行多线程的自动管理

    本文实例讲述了C#多线程学习之使用线程池进行多线程的自动管理.分享给大家供大家参考.具体如下: 在多线程的程序中,经常会出现两种情况: 一种情况:   应用程序中,线程把大部分的时间花费在等待状态,等待某个事件发生,然后才能给予响应 这一般使用ThreadPool(线程池)来解决: 另一种情况:线程平时都处于休眠状态,只是周期性地被唤醒 这一般使用Timer(定时器)来解决: ThreadPool类提供一个由系统维护的线程池(可以看作一个线程的容器),该容器需要 Windows 2000 以上系

  • C#多线程学习之(五)使用定时器进行多线程的自动管理

    本文实例讲述了C#多线程学习之使用定时器进行多线程的自动管理.分享给大家供大家参考.具体分析如下: Timer类:设置一个定时器,定时执行用户指定的函数. 定时器启动后,系统将自动建立一个新的线程,执行用户指定的函数. 初始化一个Timer对象: Timer timer = new Timer(timerDelegate, s,1000, 1000); 第一个参数:指定了TimerCallback 委托,表示要执行的方法: 第二个参数:一个包含回调方法要使用的信息的对象,或者为空引用: 第三个参

  • C#多线程学习之(二)操纵一个线程的方法

    本文实例讲述了C#多线程学习之操纵一个线程的方法.分享给大家供大家参考.具体实现方法如下: 下面我们就动手来创建一个线程,使用Thread类创建线程时,只需提供线程入口即可.(线程入口使程序知道该让这个线程干什么事) 在C#中,线程入口是通过ThreadStart代理(delegate)来提供的,你可以把ThreadStart理解为一个函数指针,指向线程要执行的函数,当调用Thread.Start()方法后,线程就开始执行ThreadStart所代表或者说指向的函数. 打开你的VS.net,新建

  • C#多线程学习之(三)生产者和消费者用法分析

    本文实例讲述了C#多线程学习之生产者和消费者用法.分享给大家供大家参考.具体实分析如下: 前面的文章说过,每个线程都有自己的资源,但是代码区是共享的,即每个线程都可以执行相同的函数.这可能带来的问题就是几个线程同时执行一个函数,导致数据的混乱,产生不可预料的结果,因此我们必须避免这种情况的发生. C#提供了一个关键字lock,它可以把一段代码定义为互斥段(critical section),互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待.在C#中,关键字lock定义如下: lock

  • Java线程通信中关于生产者与消费者案例分析

    相关方法: wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器. notify():一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程被wait,就唤醒优先级高的那个. notifyAll():一旦执行此方法,就会唤醒所有被wait的线程. 说明: 1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中. 2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器.

  • PHP进阶学习之反射基本概念与用法分析

    本文实例讲述了PHP进阶学习之反射基本概念与用法.分享给大家供大家参考,具体如下: 一.前言 Reflection(反射)是Java程序开发语言的特征之一,它允许运行中的Java程序对自身进行检查,或者说"自审",并能直接操作程序的内部属性.这一特征在实际应用中也许用得不是很多. PHP从5.0开始完美支持反射API.PHP反射可以用于观察并修改程序在运行时的行为.一个面向反射的(reflection-oriented)程序组件可以监测一个范围内的代码执行情况,可以根据期望的目标与此相

  • Python学习笔记之Break和Continue用法分析

    本文实例讲述了Python学习笔记之Break和Continue用法.分享给大家供大家参考,具体如下: Python 中的Break 和 Continue break:控制何时循环应该结束 continue: 跳过循环的一次迭代 Break 和 Continue[示例练习] 用 break 语句写一个循环,用于创建刚好长 140 个字符的字符串 news_ticker.你应该通过添加 headlines 列表中的新闻标题创建新闻提醒,在每个新闻标题之间插入空格.如果有必要的话,从中间截断最后一个

  • Python3爬虫学习之爬虫利器Beautiful Soup用法分析

    本文实例讲述了Python3爬虫学习之爬虫利器Beautiful Soup用法.分享给大家供大家参考,具体如下: 爬虫利器Beautiful Soup 前面一篇说到通过urllib.request模块可以将网页当作本地文件来读取,那么获得网页的html代码后,自然就是要将我们所需要的部分从杂乱的html代码中分离出来.既然要做数据的查找和提取,当然我们首先想到的应该是正则表达式的方式,而正则表达式书写的复杂我想大家都有体会,而且Python中的正则表达式和其他语言中的并没有太大区别,也就不赘述了

  • 深入多线程之:深入生产者、消费者队列分析

    上次我们使用AutoResetEvent实现了一个生产/消费者队列.这一次我们要使用Wait和Pulse方法来实现一个更强大的版本,它允许多个消费者,每一个消费者都在自己的线程中运行. 我们使用数组来跟踪线程. Thread[] _workers; 通过跟踪线程可以让我们在所有的线程都结束后再结束我们的队列任务. 每一个消费者线程都执行一个叫做Consume的方法,在一个for循环中,我们可以创建和启动线程.例如: 复制代码 代码如下: public PCQueue(int workerCoun

  • Java并发编程中的生产者与消费者模型简述

    概述 对于多线程程序来说,生产者和消费者模型是非常经典的模型.更加准确的说,应该叫"生产者-消费者-仓库模型".离开了仓库,生产者.消费者就缺少了共用的存储空间,也就不存在并非协作的问题了. 示例 定义一个场景.一个仓库只允许存放10件商品,生产者每次可以向其中放入一个商品,消费者可以每次从其中取出一个商品.同时,需要注意以下4点: 1.  同一时间内只能有一个生产者生产,生产方法需要加锁synchronized. 2.  同一时间内只能有一个消费者消费,消费方法需要加锁synchro

  • Java多线程之线程通信生产者消费者模式及等待唤醒机制代码详解

    前言 前面的例子都是多个线程在做相同的操作,比如4个线程都对共享数据做tickets–操作.大多情况下,程序中需要不同的线程做不同的事,比如一个线程对共享变量做tickets++操作,另一个线程对共享变量做tickets–操作,这就是大名鼎鼎的生产者和消费者模式. 正文 一,生产者-消费者模式也是多线程 生产者和消费者模式也是多线程的范例.所以其编程需要遵循多线程的规矩. 首先,既然是多线程,就必然要使用同步.上回说到,synchronized关键字在修饰函数的时候,使用的是"this"

  • Java生产者和消费者例子_动力节点Java学院整理

    生产者-消费者(producer-consumer)问题,也称作有界缓冲区(bounded-buffer)问题,两个进程共享一个公共的固定大小的缓冲区.其中一个是生产者,用于将消息放入缓冲区:另外一个是消费者,用于从缓冲区中取出消息.问题出现在当缓冲区已经满了,而此时生产者还想向其中放入一个新的数据项的情形,其解决方法是让生产者此时进行休眠,等待消费者从缓冲区中取走了一个或者多个数据后再去唤醒它.同样地,当缓冲区已经空了,而消费者还想去取消息,此时也可以让消费者进行休眠,等待生产者放入一个或者多

  • 浅谈Java中生产者与消费者问题的演变

    想要了解更多关于Java生产者消费者问题的演变吗?那就看看这篇文章吧,我们分别用旧方法和新方法来处理这个问题. 生产者消费者问题是一个典型的多进程同步问题. 对于大多数人来说,这个问题可能是我们在学校,执行第一次并行算法所遇到的第一个同步问题. 虽然它很简单,但一直是并行计算中的最大挑战 - 多个进程共享一个资源. 问题陈述 生产者和消费者两个程序,共享一个大小有限的公共缓冲区. 假设一个生产者"生产"一份数据并将其存储在缓冲区中,而一个消费者"消费"这份数据,并将

随机推荐