`
free_bird816
  • 浏览: 206049 次
  • 性别: Icon_minigender_1
  • 来自: 济南
社区版块
存档分类
最新评论

多线程--生产者和消费者

阅读更多

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


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(
-50100));
        }
    } 

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

分享到:
评论

相关推荐

    java多线程实现生产者和消费者

    在并发编程中,"生产者-消费者"模式是一种经典的解决问题的范式,用于协调两个或更多线程间的协作,其中一部分线程(生产者)生成数据,另一部分线程(消费者)消费这些数据。 生产者-消费者模型的核心在于共享资源...

    生产者-消费者多线程处理

    总之,"生产者-消费者"多线程处理是一个核心的并发编程概念,它展示了如何在多线程环境中有效地管理和共享资源,以实现高效的程序执行。理解并正确实现这一模式对于任何从事并发编程或系统设计的IT专业人员来说都至...

    Java多线程实现生产者消费者

    本示例中的“生产者-消费者”模型是一种经典的多线程问题,它模拟了实际生产环境中的资源分配与消耗过程。下面我们将详细探讨如何在Java中实现这个模型。 首先,我们要理解生产者-消费者模型的基本概念。在这个模型...

    用多线程同步方法解决生产者-消费者问题

    生产者-消费者问题是多线程同步的一个经典案例,主要探讨如何在并发环境下,确保生产者进程和消费者进程之间正确地共享资源,避免数据竞争和死锁。在这个问题中,生产者进程负责创建产品并将产品放入缓冲区,而消费...

    用多线程同步方法解决生产者-消费者问题(操作系统课设

    生产者-消费者问题是操作系统中经典的问题之一,它是指在多线程环境下,多个生产者线程和消费者线程访问同一个共享缓冲区,导致缓冲区的数据混乱和不一致的问题。 在解决生产者-消费者问题时,需要使用同步机制来...

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

    ### Java多线程-生产者与消费者问题 #### 一、生产者与消费者问题概览 **1.1 概要** 生产者与消费者问题是计算机科学中一个多线程同步的经典问题。它描述了两个线程如何共享有限资源的场景:一个是生产者...

    生产者-消费者的Linux多线程实现.pdf

    本文将详细地阐述 Linux 下利用互斥锁、条件变量、线程管理等相关函数实现多线程生产者-消费者问题,以提高资源利用率。 线程基本概念 线程是进程的一条执行路径,它包含独立的堆栈和 CPU 寄存器状态,每个线程...

    linux下的多线程实例--生产者消费者

    在Linux环境下进行多线程编程时,一个常见的应用场景就是通过生产者消费者模型来管理线程间的通信和数据共享问题。本篇文章将详细解析一个基于Linux环境下的多线程示例程序,该程序通过生产者消费者模型实现数据的...

    多线程简易实现生产者消费者模式

    生产者消费者模式是一种经典的多线程同步问题解决方案,它源于现实世界中的生产流水线,用于描述生产者(Producer)和消费者(Consumer)之间的协作关系。在这个模式中,生产者负责生成产品并放入仓库,而消费者则从...

    12.2 Qt5多线程:使用信号量实现生产者和消费者

    6. **线程间的通信**:在生产者和消费者之间,可能还需要额外的信号和槽(signal and slot)来协调他们的操作,比如当缓冲区满时通知生产者停止生产,或者当缓冲区空时通知消费者暂停消费。 通过以上步骤,我们可以...

    多线程实现生产者---消费者模型

    在操作系统实验中,实现生产者-消费者模型可以帮助我们理解线程的同步机制,如互斥锁(mutex)和信号量(semaphore)。下面我们将详细探讨这两个概念以及如何在多线程环境下应用它们。 1. **互斥锁**:互斥锁是一种...

    多线程--生产者消费者问题[1][收集].pdf

    【生产者消费者问题】是多线程编程中的经典问题,主要涉及到进程同步和互斥的概念。在操作系统中,生产者负责生成数据,而消费者则负责消耗这些数据。两者通过一个有限大小的缓冲区进行交互,当缓冲区满时,生产者...

    线程同步--生产者消费者问题

    总的来说,理解和掌握生产者消费者问题及其解决策略是Java多线程编程的重要部分,这不仅有助于编写高效、可靠的并发代码,也是提升软件设计能力的关键一步。通过不断实践和学习,开发者能够更好地应对复杂的并发场景...

    生产者和消费者模式多线程

    创建一个简单的生产者消费者模型,可以使用以下伪代码: ```java class Producer implements Runnable { private final BlockingQueue&lt;String&gt; queue; public Producer(BlockingQueue&lt;String&gt; queue) { this....

    Java多线程 生产者-消费者模式

    生产者-消费者模式是一种经典的多线程设计模式,用于解决数据共享问题,尤其是在一个线程生产数据而另一个线程消费数据的情况下。在这个模式中,生产者负责生成数据并放入共享的数据结构(如队列),而消费者则从这...

    典型的多线程--生产和消费

    "典型的多线程--生产和消费"这个主题,通常指的是生产者-消费者问题,这是一种经典的设计模式,用于解决如何在一个数据结构(如队列)中高效地存储和检索数据,同时避免生产者过于快速地生成数据而消费者无法及时...

    Java线程间的通信----生产者消费者模型

    生产者消费者模型是一种经典的线程同步问题,它模拟了实际生活中的生产过程和消费过程,使得生产者线程可以将数据生产出来,而消费者线程则负责消耗这些数据,两者之间通过共享数据结构进行协同工作。 生产者消费者...

    多线程代码 经典线程同步互斥问题 生产者消费者问题

    a: 创建一个线程 ...h: problem1 生产者消费者问题 (1生产者 1消费者 1缓冲区) problem1 more 生产者消费者问题 (1生产者 2消费者 4缓冲区) problem2 读者与写着问题 I: 信号量 semaphore 解决线程同步问题

    多线程同步方法解决生产者-消费者问题(linux线程实现)

    设计要求:(1)每个生产者和消费者对有界缓冲区进行操作后,即时显示有界缓冲区的全部内容,当前指针位置和生产者/消费者线程的标识符.(2)生产者和消费者各有两个以上.(3)多个生产者或多个消费者之间须有共享对缓冲区...

Global site tag (gtag.js) - Google Analytics