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

 更新时间:2020年6月25日 11:31  点击:2025

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

[!--infotagslink--]

相关文章

  • C# WinForm多线程解决界面卡死问题的完美解决方案,使用BeginInvoke

    问题描述:当我们的界面需要在程序运行中不断更新数据时,当一个textbox的数据需要变化时,为了让程序执行中不出现界面卡死的现像,最好的方法就是多线程来解决一个主线程来创建界...2020-06-24
  • C#实现简单的登录界面

    我们在使用C#做项目的时候,基本上都需要制作登录界面,那么今天我们就来一步步看看,如果简单的实现登录界面呢,本文给出2个例子,由简入难,希望大家能够喜欢。...2020-06-25
  • 浅谈C# 字段和属性

    这篇文章主要介绍了C# 字段和属性的的相关资料,文中示例代码非常详细,供大家参考和学习,感兴趣的朋友可以了解下...2020-11-03
  • C#中截取字符串的的基本方法详解

    这篇文章主要介绍了C#中截取字符串的的基本方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-11-03
  • C#实现简单的Http请求实例

    这篇文章主要介绍了C#实现简单的Http请求的方法,以实例形式较为详细的分析了C#实现Http请求的具体方法,需要的朋友可以参考下...2020-06-25
  • C#连接SQL数据库和查询数据功能的操作技巧

    本文给大家分享C#连接SQL数据库和查询数据功能的操作技巧,本文通过图文并茂的形式给大家介绍的非常详细,需要的朋友参考下吧...2021-05-17
  • C#中new的几种用法详解

    本文主要介绍了C#中new的几种用法,具有很好的参考价值,下面跟着小编一起来看下吧...2020-06-25
  • 使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序)

    这篇文章主要介绍了使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25
  • C#开发Windows窗体应用程序的简单操作步骤

    这篇文章主要介绍了C#开发Windows窗体应用程序的简单操作步骤,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-04-12
  • C#从数据库读取图片并保存的两种方法

    这篇文章主要介绍了C#从数据库读取图片并保存的方法,帮助大家更好的理解和使用c#,感兴趣的朋友可以了解下...2021-01-16
  • C#和JavaScript实现交互的方法

    最近做一个小项目不可避免的需要前端脚本与后台进行交互。由于是在asp.net中实现,故问题演化成asp.net中jiavascript与后台c#如何进行交互。...2020-06-25
  • C++调用C#的DLL程序实现方法

    本文通过例子,讲述了C++调用C#的DLL程序的方法,作出了以下总结,下面就让我们一起来学习吧。...2020-06-25
  • 轻松学习C#的基础入门

    轻松学习C#的基础入门,了解C#最基本的知识点,C#是一种简洁的,类型安全的一种完全面向对象的开发语言,是Microsoft专门基于.NET Framework平台开发的而量身定做的高级程序设计语言,需要的朋友可以参考下...2020-06-25
  • C#变量命名规则小结

    本文主要介绍了C#变量命名规则小结,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-09-09
  • C#绘制曲线图的方法

    这篇文章主要介绍了C#绘制曲线图的方法,以完整实例形式较为详细的分析了C#进行曲线绘制的具体步骤与相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下...2020-06-25
  • C# 中如何取绝对值函数

    本文主要介绍了C# 中取绝对值的函数。具有很好的参考价值。下面跟着小编一起来看下吧...2020-06-25
  • c#自带缓存使用方法 c#移除清理缓存

    这篇文章主要介绍了c#自带缓存使用方法,包括获取数据缓存、设置数据缓存、移除指定数据缓存等方法,需要的朋友可以参考下...2020-06-25
  • c#中(&&,||)与(&,|)的区别详解

    这篇文章主要介绍了c#中(&&,||)与(&,|)的区别详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-06-25
  • 经典实例讲解C#递归算法

    这篇文章主要用实例讲解C#递归算法的概念以及用法,文中代码非常详细,帮助大家更好的参考和学习,感兴趣的朋友可以了解下...2020-06-25
  • C#学习笔记- 随机函数Random()的用法详解

    下面小编就为大家带来一篇C#学习笔记- 随机函数Random()的用法详解。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25