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

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()发出的“脉冲”。

它的执行结果很简单:

Code
Produce: 1
Consume: 1
Produce: 2
Consume: 2
Produce: 3
Consume: 3


Produce: 20
Consume: 20 

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

C#多线程学习 系列教程(强烈推荐)

§1. C#多线程学习(一) 多线程的相关概念(转载系列)——继续搜索引擎研究

http://www.cnblogs.com/OceanChen/archive/2009/02/02/1382233.html

§2. C#多线程学习(二) 如何操纵一个线程 (转载系列)——继续搜索引擎研究

http://www.cnblogs.com/OceanChen/archive/2009/02/02/1382232.html

§3. C#多线程学习(三) 生产者和消费者 (转载系列)——继续搜索引擎研究

http://www.cnblogs.com/OceanChen/archive/2009/02/02/1382231.html

§4. C#多线程学习(四) 多线程的自动管理(线程池) (转载系列)——继续搜索引擎研究

http://www.cnblogs.com/OceanChen/archive/2009/02/02/1382230.html

§5. C#多线程学习(五) 多线程的自动管理(定时器) (转载系列)——继续搜索引擎研究

http://www.cnblogs.com/OceanChen/archive/2009/02/02/1382229.html

§6. C#多线程学习(六) 互斥对象(转载系列)——继续搜索引擎研究

http://www.cnblogs.com/OceanChen/archive/2009/02/02/1382227.html

文章出处:http://www.cnblogs.com/xugang/archive/2008/04/06/1138856.html

转载于:https://www.cnblogs.com/OceanChen/archive/2009/02/02/1382231.html

C#多线程学习(三) 生产者和消费者 (转载系列)——继续搜索引擎研究相关推荐

  1. C#多线程学习(四) 多线程的自动管理(线程池) (转载系列)——继续搜索引擎研究...

    在多线程的程序中,经常会出现两种情况: 一种情况:   应用程序中,线程把大部分的时间花费在等待状态,等待某个事件发生,然后才能给予响应                   这一般使用ThreadPo ...

  2. C#多线程学习(三) 生产者和消费者

    C#多线程学习(三) 生产者和消费者 原文链接:http://kb.cnblogs.com/page/42530/ 本系列文章导航 C#多线程学习(一) 多线程的相关概念 C#多线程学习(二) 如何操 ...

  3. [转]C#多线程学习(三) 生产者和消费者

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

  4. C#多线程学习(三) 生产者和消费者 2

    下面定义生产者类 CellProd 和消费者类 CellCons ,它们都只有一个方法ThreadRun(),以便在Main()函数中提供给线程的ThreadStart代理对象,作为线程的入口. pu ...

  5. C#多线程学习(三) 生产者和消费者 1——解决线程间冲突的关键

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

  6. C#多线程学习(五) 多线程的自动管理(定时器) (转载系列)——继续搜索引擎研究...

    Timer类:设置一个定时器,定时执行用户指定的函数.               定时器启动后,系统将自动建立一个新的线程,执行用户指定的函数. 初始化一个Timer对象: Timer timer ...

  7. Java多线程学习三:有哪几种实现生产者消费者模式的方法

    我们先来看看什么是生产者消费者模式,生产者消费者模式是程序设计中非常常见的一种设计模式,被广泛运用在解耦.消息队列等场景.在现实世界中,我们把生产商品的一方称为生产者,把消费商品的一方称为消费者,有时 ...

  8. java多线程生产者与消费者案例_多线程操作实例——生产者与消费者

    面对多线程学习生产者与消费者是最基本的实例 对于java后端开发的人员必须要掌握,还有考研考试计算机操作系统的同鞋. 下面是三个实例对于生产者与消费者的的例子,层层递进,逐步解决问题. 问题:生产者- ...

  9. Python爬虫的经典多线程方式,生产者与消费者模型

    在之前的文章当中我们曾经说道,在多线程并发的场景当中,如果我们需要感知线程之间的状态,交换线程之间的信息是一件非常复杂和困难的事情.因为我们没有更高级的系统权限,也没有上帝视角,很难知道目前运行的状态 ...

最新文章

  1. [ACM]n a^o7 !
  2. TensorFlow实战-AlexNet
  3. jquery --- 控制元素的隐藏/显示
  4. 数据结构链表之单向链表:Python3 实现单向链表——1
  5. python找不到文件数据_如何用 Python 正确读取资源文件
  6. 商城管理系统(前台+后台+管理员+用户+html+jsp)
  7. java中通过正则表达式提取数字
  8. gba模拟器html5源码,酷!用 JS 做的 GBA 模拟器
  9. A*算法解决传教士—野人过河问题
  10. 免流混淆 一 待完善(更新中)
  11. 名帖196 米芾 行书《诉衷情》
  12. 腾讯微信公众平台账号类型说明
  13. android 白色圆点,Android通知图标是一个白色圆圈
  14. 【宏定义】#define 的使用方法
  15. H.266/VVC-VTM代码学习18-自适应QP设置(Adaptive QP)
  16. SpeedTree:树模型制作软件的下载与安装
  17. 2021-08-30备货赛灵思Xilinx公司的7系列FPGA选择参考分类
  18. 数据归约——主成分分析PCA
  19. EXCEL日期格式要双击一下单元格才变正确格式
  20. linux oracle加固,Oracle的一些安全加固配置

热门文章

  1. mysql〉_MySql 基础知识-常用命令及sql语句
  2. 计算机视觉基础:图像处理Task01-图像插值算法
  3. 用c语言elgamal共密钥密码加密算法,非对称密钥体制RSA加密原理
  4. glide源码中包含了那种设计模式_源码中的设计模式-单例模式
  5. 拖动时候的样式怎么改_你对“挡拆”的死板印象是时候要改了!看看欧文、保罗都是怎么做的吧。...
  6. java源码依赖分析_Spring项目运行依赖spring-contex解析
  7. java中的多态性_[转载] c++多态与java多态性_Java中的多态性
  8. yum 安装mysql 5.0_CentOS 通过 yum 安装 Mysql 5.0
  9. 标准的LSTM网络以及公式
  10. 文件/目录权限设置命令:chmod