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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/34780.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

mousedown拖拽功能(vue3+ts)

因为项目有rem适配&#xff0c;使用第三方插件无法处理适配问题&#xff0c;所有只能自己写拖拽功能了 拖拽一般都会想到按下&#xff0c;移动&#xff0c;放开&#xff0c;但是本人亲测&#xff0c;就在div绑定一个按下事件就行了&#xff08;在事件里面写另外两个事件&#x…

爬虫ip池越大越好吗?

作为一名资深的程序员&#xff0c;今天我要给大家分享一些关于爬虫ip池的知识。关于ip代理池的问题&#xff0c;答案是肯定的&#xff0c;池子越大越好。下面跟我一起来盘点一下ip池大的好处吧&#xff01; 1、提高稳定性 爬虫ip池越大&#xff0c;意味着拥有更多可用的爬虫ip…

「C/C++」C/C++搭建程序框架

✨博客主页何曾参静谧的博客&#x1f4cc;文章专栏「C/C」C/C程序设计&#x1f4da;全部专栏「UG/NX」NX二次开发「UG/NX」BlockUI集合「VS」Visual Studio「QT」QT5程序设计「C/C」C/C程序设计「Win」Windows程序设计「DSA」数据结构与算法「File」数据文件格式 目录 1. 分离职…

Flume原理剖析

一、介绍 Flume是一个高可用、高可靠&#xff0c;分布式的海量日志采集、聚合和传输的系统。Flume支持在日志系统中定制各类数据发送方&#xff0c;用于收集数据&#xff1b;同时&#xff0c;Flume提供对数据进行简单处理&#xff0c;并写到各种数据接受方&#xff08;可定制&…

使用阿里云服务器搭建Discuz论坛网站教程基于CentOS系统

阿里云百科分享使用阿里云服务器建站教程&#xff0c;本文是搭建Discuz论坛&#xff0c;Discuz!是一款通用的社区论坛软件系统&#xff0c;它采用PHP和MySQL组合的基础架构&#xff0c;为您提供高效的论坛解决方案。本文介绍如何在CentOS 7操作系统的ECS实例上搭建Discuz! X3.4…

Nginx 安装与部署

文章和代码已经归档至【Github仓库&#xff1a;https://github.com/timerring/front-end-tutorial 】或者公众号【AIShareLab】回复 nginx 也可获取。 文章目录 虚拟机安装CentOS7.4Linux配置配置上网配置静态ip Nginx的安装版本区别备份克隆 安装编译安装报错解决 启动Nginx防…

topo 成绩排名

题目描述 每到考试后&#xff0c;学校都会发成绩表给每个学生&#xff0c;但是很多同学更关心的是自己在班级里的排名&#xff0c;可惜排名信息并没有公开。 小雯同学很想知道这次期末考试的全班排名情况&#xff0c;但是她的同学却不愿意告诉她自己的分数&#xff0c;只告诉她…

分布式 - 消息队列Kafka:Kafka生产者发送消息的方式

文章目录 1. Kafka 生产者2. kafaka 命令行操作3. kafka 生产者发送消息流程4. Kafka 生产者的创建5. Kafka 生产者发送消息1. 发送即忘记2. 同步发送3. 异步发送 6. Kafka 消息对象 ProducerRecord 1. Kafka 生产者 不管是把Kafka作为消息队列、消息总线还是数据存储平台&…

wpf控件上移下移,调整子集控件显示顺序

页面代码: <!-- 导出A2,自定义导出设置列,添加时间:2023-8-9 14:14:18,作者:whl; --><Window x:Class="WpfSnqkGasAnalysis.WindowGasExportA2"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http:/…

git远程仓库的创建及使用

1.仓库的概念&#xff1a; 1.1 本地仓库&#xff1a; 了解远程仓库前我们先了解一下本地仓库&#xff0c;本地仓库开发人员在完成部分代码的编写之后&#xff0c;可以将这一部分的代码做一个提交。这个提交完全就是一个新的版本提交&#xff0c;当然这个提交动作是在开发者的电…

CSS革命:用Sass/SCSS引领前端创新

目录 前言SCSSSassSass 和 SCSS 的区别 前言 在现代的前端开发中&#xff0c;CSS已成为呈现网页和应用程序样式的核心。然而&#xff0c;原生的CSS语法在大型项目中可能变得混乱、冗长且难以维护。 为了解决这些问题&#xff0c;SCSS&#xff08;Sass CSS&#xff09;和Sass&am…

Java基础篇--数组

目录 声明和初始化数组&#xff1a; 访问和修改数组元素&#xff1a; 数组长度&#xff1a; 遍历数组&#xff1a; 多维数组的遍历&#xff1a; 数组的常见操作和方法&#xff1a; 拓展小知识&#xff1a; 数组是Java中的一种数据结构&#xff0c;用于存储相同类型的多个…

B100-技能提升-线程池分布式锁

目录 线程池什么是线程池&#xff1f;为什么用线程池?线程池原理常见四种线程池和自定义线程池 线程池 什么是线程池&#xff1f; 池化技术 为什么用线程池? 1 由于设置最大线程数&#xff0c;防止线程过多而导致系统崩溃。 2 线程复用&#xff0c;不需要频繁创建或销毁…

包管理机制pip3

pip3 安装pip3 安装pip3 apt install python3-pip yum install python3-pip从仓库出发的命令 查询仓库信息 // 获取默认pip3源 pip3 config get global.index-url查询所有软件包 查询已经安装的所有软件包 pip3 list从软件包出发的命令 从软件包名出发查询其他信息 查询…

230. 二叉搜索树中第K小的元素

介绍 中序遍历&#xff1a;左子树 -> 中 -> 右子树 二叉搜索树&#xff1a;中序遍历可以得到有序的序列 递归法 1.使用函数循环递归处理 2.使用一个数组来保存 k, 保证在个个递归函数中都能看到 看的变化&#xff1b;每访问一个节点&#xff0c;这个数减一&#xff0c…

软件测试基础篇——Redis

Redis Redis数据库的配置与连接 解压redis数据库的安装包&#xff08;建议把解压后的安装包放到磁盘的根目录&#xff0c;方便访问操作&#xff09;打开【命令行窗口】&#xff1a;winR在命令行窗口&#xff0c;进入到redis安装目录中 ​ 格式一&#xff1a;cd /d redis目录…

Linux安装Zookeeper

1、Zookeeper简介 ZooKeeper是一个分布式的&#xff0c;开放源码的分布式应用程序协调服务&#xff0c;是Google的Chubby一个开源的实现&#xff0c;是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件&#xff0c;提供的功能包括&#xff1a;配置维护、域…

自然语言处理从入门到应用——LangChain:记忆(Memory)-[记忆的类型Ⅲ]

分类目录&#xff1a;《自然语言处理从入门到应用》总目录 对话令牌缓冲存储器ConversationTokenBufferMemory ConversationTokenBufferMemory在内存中保留了最近的一些对话交互&#xff0c;并使用标记长度来确定何时刷新交互&#xff0c;而不是交互数量。 from langchain.me…

基于灰狼优化(GWO)、帝国竞争算法(ICA)和粒子群优化(PSO)对梯度下降法训练的神经网络的权值进行了改进(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

环保行业如何开发废品回收微信小程序

废品回收是近年来受到越来越多人关注的环保行动。为了推动废品回收的普及和方便&#xff0c;我们可以利用微信小程序进行制作&#xff0c;方便人们随时随地参与废品回收。 首先&#xff0c;我们需要注册并登录乔拓云账号&#xff0c;并进入后台。乔拓云是一个提供微信小程序制作…