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

2016-02-19 13:08 33 1 收藏

岁数大了,QQ也不闪了,微信也不响了,电话也不来了,但是图老师依旧坚持为大家推荐最精彩的内容,下面为大家精心准备的C#多线程学习(三) 生产者和消费者,希望大家看完后能赶快学习起来。

【 tulaoshi.com - 编程语言 】

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

C#提供了一个关键字lock,它可以把一段代码定义为互斥段(critical section),互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待。在C#中,关键字lock定义如下:

lock(expression) statement_block

expression代表你希望跟踪的对象,通常是对象引用。

如果你想保护一个类的实例,一般地,你可以使用this;
如果你想保护一个静态变量(如互斥代码段在一个静态方法内部),一般使用类名就可以了。
而statement_block就是互斥段的代码,这段代码在一个时刻内只可能被一个线程执行。

下面是一个使用lock关键字的典型例子,在注释里说明了lock关键字的用法和用途。

(本文来源于图老师网站,更多请访问http://www.tulaoshi.com/bianchengyuyan/)

示例如下:

 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;

(本文来源于图老师网站,更多请访问http://www.tulaoshi.com/bianchengyuyan/)

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

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

来源:http://www.tulaoshi.com/n/20160219/1603064.html

延伸阅读
单个写入程序/多个阅读程序在.Net类库中其实已经提供了实现,即System.Threading.ReaderWriterLock类。本文通过对常见的单个写入/多个阅读程序的分析来探索c#的多线程编程。 问题的提出 所谓单个写入程序/多个阅读程序的线程同步问题,是指任意数量的线程访问共享资源时,写入程序(线程)需要修改共享资源,而阅读程序(线程)...
携程如何获消费者劵?   近日,携程网信用卡事件,让携程的印象一夜跌到谷底。但事后携程道歉的态度还让人满意。小编认为账号泄露这些事,在当今互联网中已经不是什么大事,只要自己在绑定的时候小心一点就可以。你在使用这样的服务时就需要承受这样的风险。为了方便,用户还是会使用的。 就如今天小编向大家介绍的这个活动,只...
标签: 电脑入门
此前微软已推出了Windows 10科技预览版,如果你感兴趣的话,现在就可下载和安装,不过该版本仅用于电脑。今年9月,微软首次介绍这款下一代Windows版系统时,它主要呈现的是开发人员关注的系统方面的功能,但也透露了一些消费者导向方面的东西。 当时微软也承诺未来也会推出一款Windows 10消费者版本,最新传闻它将在明年1月发布。 尽管微...
我们在写Remoting程序或者其他的一些应用程序的时候难免要和线程打交道,.Net使我们很容易就可以创建一个线程,但是它提供的创建线程和启动线程的方法没有明显的提供参数,假如我们要用线程来启动类里面一个带参数的方法该怎么办?下面就简单的介绍如何使用.NET提供的丰富的框架来实现这个功能。为了可以生动详细的介绍整个过程,我建立下面的一...
深受消费者喜爱的珍珠奶茶和印度奶茶 珍珠奶茶 于台湾盛行的珍珠奶茶,则于奶茶内加入粉圆,煮熟的粉圆外观乌黑晶透,遂以“珍珠”命名,另可加入各式各样的配料,调制出不同的味道。丝袜奶茶传统的英式奶茶以茶为主,只下少量的浓牛奶,杯的体积较小,一般于早餐、下午茶或晚餐后聊天时饮用。 印度奶茶 ...

经验教程

274

收藏

21
微博分享 QQ分享 QQ空间 手机页面 收藏网站 回到头部