在多线程编程中,如果每个线程的运行不是完全独立的。那么,一个线程执行到某个时刻需要知道其他线程发生了什么。嗯,这就是所谓线程同步。同步事件对象(XXXEvent)有两种行为:

1、等待。线程在此时会暂停运行,等待其他线程发出信号才继续(等你约);

2、发出信号。当前线程发出信号,其他正在等待线程收到信号后继续运行(我约你)。

从前,小明、小伟、小更、小红、小黄计划到野外去烤鱼吃。但他们只确定市郊东南方向的一片区域,并不能保证具体哪个地点适合烧烤。于是,他们商量好,大家同时从家里出发。小明离那里比较近,他先去考察一下;其他人到了东南郊后集合,等小明的消息。小明考察完毕,向大家群发消息说明选定的地点是F。最后大家继续前行,奔向F。

等待事件有好几个:

1、Mutex:互斥体。一次只能有一个线程获取到互斥体,其他线程只能等。占用互斥体的线程释放后,其他线程继续抢 Mutex。然后只有一个线程能抢到,其他线程继续等……

2、AutoResetEvent:自动事件,发出信号后立刻重置。

3、ManualResetEvent:手动事件,发出信号后不会立刻重置,得手动重置。

4、CountdownEvent:这个和上面两个差不多。但它会设定一个计数,线程发出信号时会减少计数。被阻止的线程要等到计数 <= 0 时才获得信号。

本次咱们讨论的重点是看看自动重置信号和手动重置信号之间有什么区别。

先看看自动重置的。

internal class Program
{ static AutoResetEvent theEvent = new(false); static void Main(string[] args)
{
// 启动三个线程
ThreadPool.QueueUserWorkItem(DoWorking, "A");
ThreadPool.QueueUserWorkItem(DoWorking, "B");
ThreadPool.QueueUserWorkItem(DoWorking, "C");
// 主线程监听键盘消息
while(true)
{
var keyInfo = Console.ReadKey(true);
// 看看是不是Y键
if(keyInfo.Key == ConsoleKey.Y)
{
// 点亮信号
theEvent.Set();
}
// 输出一行,方便判断一个循环
Console.WriteLine("------------------------------");
}
} static void DoWorking(object? state)
{
while(true)
{
// 等待主线程的信号
// 此线程会暂停
theEvent.WaitOne();
// 得到信号了,继续运行
Console.WriteLine("{0}已收到通知", state);
}
}
}

这个例子创建了三个线程,这里我用的是线程池,把一个WaitCallback委托传给 QueueUserWorkItem 方法就可以在线程池中运行新线程。上面示例中绑定的方法是 DoWorking。

AutoResetEvent 类的构造函数传了一个 bool 值,它的作用是设置等待事件的初始状态:

1、如果为 true,表示事件初始状态为打开信号,这会使正在等的线程马上得到信号;

2、如果为 false,表示事件的初始状态为没有信号,正在等待的线程继续等。

按照咱们这个例子的实际情况,我们一开始应该让事件无状态,让后台的三个线程等待。主线程读取按键信息,如果按的是【Y】键,那么事件调用 Set 方法,打开信号。此时,等得花儿都谢了的三个线程会继续。我们运行一下,看看能否符合预期。

经测试,我们会发现:每次按【Y】后,三个线程中只有一个获得信号并继续,其他两个还在高速上堵车。 AutoResetEvent 的自动重置就是打开信号后又立马关闭,每次只让一个线程收到信号。所以,当咱们按一次【Y】键后,主线程发出了信号,又马上关闭。三个后台线程相互竞争,随机获得机会,结束等待并继续运行。

手动重置事件在打开信号后,信号会持续有效,直到调用 Reset 方法手动关闭信号。手动重置信号能让多个线程有足够的时间收到信号。

下面咱们把上面的示例改为使用 ManualResetEvent 类。

internal class Program
{
static ManualResetEvent theEvent = new(false); static void Main(string[] args)
{
// 启动三个线程
ThreadPool.QueueUserWorkItem(DoWorking, "A");
ThreadPool.QueueUserWorkItem(DoWorking, "B");
ThreadPool.QueueUserWorkItem(DoWorking, "C");
// 主线程监听键盘消息
while(true)
{
var keyInfo = Console.ReadKey(true);
// 看看是不是Y键
if(keyInfo.Key == ConsoleKey.Y)
{
// 点亮信号
theEvent.Set(); // 持续一段时间后关闭信号
Thread.Sleep(3);
theEvent.Reset();
}
// 输出一行,方便判断一个循环
Console.WriteLine("------------------------------");
}
} static void DoWorking(object? state)
{
while(true)
{
// 等待主线程的信号
// 此线程会暂停
theEvent.WaitOne();
// 得到信号了,继续运行
Console.WriteLine("{0}已收到通知", state);
}
}
}

然后运行程序,这一次按下【Y】键后,三个线程都能收到信号通知了。

你会发现,有些线程重复了多次,那是因为 DoWorking 方法里面是个死循环。当信号持续打开期间,三个线程都有机会收到信号,甚至会重复收到。

上面的东东纯属演示,实际使用的话不会这样设计。最好的方法是建一个列表对象,主线程接收到的按键字符存放到一个列表中,然后,后台线程不断地从列表中取出元素来处理。这样设计程序会更流畅。

internal class Program
{
#region 字段区域
static Queue<char> keyChars = new();
#endregion static void Main(string[] args)
{
// 启动三个线程
ThreadPool.QueueUserWorkItem(DoSomething, "A");
ThreadPool.QueueUserWorkItem(DoSomething, "B");
ThreadPool.QueueUserWorkItem(DoSomething, "C"); while(true)
{
// 读取键盘字符
ConsoleKeyInfo info = Console.ReadKey(true);
// 将字符放入队列
keyChars.Enqueue(info.KeyChar);
}
} static void DoSomething(object? state)
{
while(true)
{
// 锁定
Monitor.Enter(keyChars);
if (keyChars.Count > 0)
{
// 取掉一个元素
char c = keyChars.Dequeue();
Console.WriteLine($"线程【{state}】获得字符:{c}");
}
// 解锁
Monitor.Exit(keyChars);
}
}
}

这里我用泛型队列 Queue<T> 来存放键盘敲入的字符,DoSomething 方法将放入线程池中运行。在从队列中取出元素并处理时,一定要记得上锁。我用的是 Monitor 对象的静态方法来上锁和解锁,当然你可以用 lock 语句块。

lock(keyChars)
{
……
}

如果不上锁,线程间在抢占资源时会导致不一致的状态。当A线程访问 keyChars.Count 属性时得到 1,还是 > 0 的,但在取出最后一个元素前,偏偏B线程动作快把最后一个元素拿走了。当A线程执行到 keyChars.Dequeue() 一句时,keyChars 队列中已经没有元素了,会发生错误。

主线程在 Enqueue 时并不需要锁定,因为元素送入队列只有一个线程在做,没人跟他抢资源,可以不锁定。

运行程序后,可以按字母、数字等按键来测试。毕竟像【F3】、【Ctrl】等按键获取到的是空白 char。

这样就顺畅很多了。

【.NET】多线程:自动重置事件与手动重置事件的区别的更多相关文章

  1. [一个经典的多线程同步问题]解决方案二:Event事件

    使用关键段来解决经典的多线程同步互斥问题,由于关键段的“线程所有权”特性所以关键段只能用于线程的互斥而不能用于同步.本篇介绍用事件Event来尝试解决这个线程同步问题. 首先介绍下如何使用事件.事件E ...

  2. 秒杀多线程第六篇 经典线程同步 事件Event

    原文地址:http://blog.csdn.net/morewindows/article/details/7445233 上一篇中使用关键段来解决经典的多线程同步互斥问题,由于关键段的“线程所有权” ...

  3. 转--- 秒杀多线程第六篇 经典线程同步 事件Event

    阅读本篇之前推荐阅读以下姊妹篇: <秒杀多线程第四篇 一个经典的多线程同步问题> <秒杀多线程第五篇 经典线程同步关键段CS> 上一篇中使用关键段来解决经典的多线程同步互斥问题 ...

  4. C# 并行编程 之 轻量级手动重置事件的使用

    目录(?)[-] 简单介绍 使用超时和取消 跨进程或AppDomain的同步   简单介绍 如果预计操作的等待的时间非常短,可以考虑使用轻量级的手动重置事件,ManualResetEventSlim. ...

  5. 关于SpringKafka消费者的几个监听器:[一次处理单条消息和一次处理一批消息]以及[自动提交offset和手动提交offset]

    自己在使用Spring Kafka 的消费者消费消息的时候的实践总结: 接口 KafkaDataListener 是spring-kafka提供的一个供消费者接受消息的顶层接口,也是一个空接口; pu ...

  6. jQuery 学习笔记(5)(事件绑定与解绑、事件冒泡与事件默认行为、事件的自动触发、自定义事件、事件命名空间、事件委托、移入移出事件)

    1.事件绑定: .eventName(fn) //编码效率略高,但部分事件jQuery没有实现 .on(eventName, fn) //编码效率略低,所有事件均可以添加 注意点:可以同时添加多个相同 ...

  7. 新引入thinkphp报错“应用目录[./Application/]不可写,目录无法自动生成! 请手动生成项目目录~”

    新引入thinkphp报错“应用目录[./Application/]不可写,目录无法自动生成! 请手动生成项目目录~”, 其主要原因是文件夹的权限问题,手动将项目文件夹权限更改为可读可写就OK,具体操 ...

  8. PowerBuilder中DW如何手动触发事件

    调用setitem默认不会触发itemchanged事件 如果想实现可手动触发itemchanged事件 事件格式如下: dw_list.event itemchanged( /*long row*/ ...

  9. springboot自动配置原理以及手动实现配置类

    springboot自动配置原理以及手动实现配置类 1.原理 spring有一个思想是"约定大于配置". 配置类自动配置可以帮助开发人员更加专注于业务逻辑开发,springboot ...

  10. [JavaScript] JavaScript事件注册,事件委托,冒泡,捕获,事件流

    面试题 event 事件 事件委托是什么? 如何阻止事件冒泡,阻止默认事件呢? Javascript 的事件流模型都有什么? 事件绑定和普通事件有什么区别? Event 对象 Event 对象,当事件 ...

随机推荐

  1. [爬虫]3.4.1 Scrapy框架的基本使用

    Scrapy是一款强大的Python网络爬虫框架,它可以帮助你快速.简洁地编写爬虫程序,处理数据抓取.处理和存储等复杂问题. 1. 安装Scrapy 在开始使用Scrapy之前,你需要先将其安装在你的 ...

  2. React组件设计之性能优化篇

    我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品.我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值. 本文作者:空山 前言 由于笔者最近在开发中遇到了一个重复渲染导致子组 ...

  3. Django:TypeError: view must be a callable or a list/tuple in the case of include().

    错误: path("uploads/(?P<path>.*)$", 'django.views.static.serve', {"document_root& ...

  4. [minio]简介与安装

    简介 MinIO是一款高性能的分布式对象存储系统. 官网地址 特性 轻便 高性能 跨平台 高扩展性 云原生支持 兼容Amazon S3 基本概念 s3:simple storage service,简 ...

  5. LVS专访阿里云席明贤,从视频云2.0到“数能生智”的超长畅谈

    这是一篇人物专访,源自LiveVideoStack与阿里云视频云负责人席明贤(花名右贤)的对话.面对风云变幻的内外环境,阿里云在视频云赛道是坚定向前的,右贤没有回避多媒体当下行业面临的困难以及业务面临 ...

  6. Cookies 完全指南

    我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品.我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值. 本文作者:佳岚 前言 Cookie实际上是一小段的文本信息,它产生的 ...

  7. 手写RISC-V处理器(1)

    由来 由于去年工作变动,有幸进入了芯片行业,但主要工作内容为基于RISC-V的嵌入式应用软件开发,几乎接触不到芯片设计的相关知识,然而随着工作的深入,越来越想探究一下运行在软件之下的CPU的世界,于是 ...

  8. IDEA 22.2.3 创建web项目及Tomcat部署与服务器初始界面修改(保姆版)

    开始前请确认自己的Tomcat.JDK已经安装配置完毕 不同版本的IDEA创建配置流程可能不同,演示中的IDEA版本号为22.2.3 本教程创作时间为2023/09/14 1.创建项目 通过下图路径进 ...

  9. 2023-09-16:用go语言,给你一个整数 n 和一个在范围 [0, n - 1] 以内的整数 p , 它们表示一个长度为 n 且下标从 0 开始的数组 arr , 数组中除了下标为 p 处是 1

    2023-09-16:用go语言,给你一个整数 n 和一个在范围 [0, n - 1] 以内的整数 p , 它们表示一个长度为 n 且下标从 0 开始的数组 arr , 数组中除了下标为 p 处是 1 ...

  10. 文心一言 VS 讯飞星火 VS chatgpt (94)-- 算法导论9.2 3题

    三.用go语言,给出 RANDOMIZED-SELECT的一个基于循环的版本. 文心一言: 在循环中实现 RANDOMIZED-SELECT 的一个版本可以如下所示.这个版本使用 Go 语言编写,利用 ...