首页 > 编程语言 >C#多线程学习(三) 生产者和消费者

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

时间:2023-04-21 09:22:12浏览次数:53  
标签:balance Monitor C# 生产者 int 对象 线程 new 多线程

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

线程学习第一篇: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

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

标签:balance,Monitor,C#,生产者,int,对象,线程,new,多线程
From: https://www.cnblogs.com/x-zhi/p/17335461.html

相关文章

  • 网络流的C++代码实现与过程讲解
    网络流是一种非常重要的图论算法,它在许多实际问题中得到广泛应用。本文将介绍网络流算法的C++代码实现与过程讲解。算法概述网络流算法是通过将图中的边看作流量通道,将图的点看作流量的起点或终点,来求解图中的最大或最小流量的问题。它是一种非常重要的最优化算法,广泛应用于图论......
  • python通过psutil获取服务器cpu,内存,磁盘使用率
    psutil是一个跨平台的Python库,它允许你获取有关系统进程和系统资源使用情况的信息。它支持Windows、Linux、OSX、FreeBSD、OpenBSD和NetBSD等操作系统,并提供了一些非常有用的功能,如:获取系统CPU使用率、内存使用率、磁盘使用率等信息。获取进程列表、进程状态、进程CPU使用率、......
  • LogBack 没有打印日志
    背景:某日进行测试,新增了一行日志(项目使用的是logback)报错:无,就是不打印日志解决:经过仔细查看代码,发现之前的人写代码的时候在其它类里面,将privatefinalLoggerlog=LoggerFactory.getLogger(XXXX.class);在Logger工厂中,获取静态绑定的Logger实......
  • Spring的Factories机制介绍
    Java的SPI机制JavaSpringBoot加载yml配置文件中字典项Spring的Factories就是Spring版本的JavaSpi。SpringFactories的最重要的功能就是:可以通过配置文件指定Spring容器加载一些特定的组件。SpringFactories是一种类似于JavaSPI的机制,它在META-INF/spring.factories......
  • 二、STM32Cube生态系统操作
    一、嵌入式硬件二、嵌入式软件三、核心板设计3.1电源供电3.2时钟电路3.3复位电路3.4调试电路四、外围电路设计4.1ArduinoUNO引脚4.2指示灯电路4.3按键电路五、STM32CubeMX软件5.1目标板选择5.2引脚分配5.3外设配置......
  • leetcode-876链表的中间节点
    找链表的中间节点思路心得当不知道while的终止条件时,可以先写while(true),然后在循环体中写终止条件,这样写的好处是可以暂时不考虑终止条件,使思路更清晰;坏处是这样有时候会使循环体的内容很混乱要注意分类!本题中把情况分为节点个数是奇数和偶数去分析,最终找到统一的......
  • GLIBC2.36利用obstack去劫持执行流
    GLIBC2.36中利用obstack去劫持执行流作者没有起名字,可能就是跟houseofapple太相似了,就是roderick师傅提出的houseofapple中没有发现的一个链,个人感觉就是houseofapple跟houseofbanana的一个结合(说实话这两个我已经快忘了怎么用的了所以会将这个攻击封装成几个函数以应......
  • net core 6 部署到ubuntu
    一、安装dotnetSDK 1.更新源sudoapt-getupdate;2.启用MicrosoftPPAwgethttps://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.debsudodpkg-ipackages-microsoft-prod.deb3.安装.NETCoreSDKsudoaptinstallapt-transport-ht......
  • 【Visual Leak Detector】源码下载
    说明使用VLD内存泄漏检测工具辅助开发时整理的学习笔记。本篇介绍VLD源码的下载。同系列文章目录可见《内存泄漏检测工具》目录目录说明1.下载途径2.不同下载途径的源文件差异1.下载途径以v2.5.1版本为例,可以到Github-KindDragon-vld页面下载master的zip源码......
  • oracle数字类函数
    Oracle数据库中所有的数字类函数:ABS:返回指定数值的绝对值ACOS:返回指定角度的反余弦值ASIN:返回指定角度的反正弦值ATAN:返回指定数字的反正切值ATAN2:返回两个数值的反正切值CEIL:返回大于或等于指定数字的最小整数(向上取整)COS:返回指定角度的余弦值COSH:返回......