在多线程编程中,如果每个线程的运行不是完全独立的。那么,一个线程执行到某个时刻需要知道其他线程发生了什么。嗯,这就是所谓线程同步。同步事件对象(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. 你的Spring应用启动很慢?不妨试试这个工具!

    睡不着闲逛,在GitHub上看到一个挺实用的开源项目:Spring Startup Analyzer. 从项目名称中就大概能猜到,这是一个分析Spring应用启动过程的工具.Spring Startu ...

  2. linux 字符集与编码格式相关

    字符集:​多个字符的集合. # 书写系统字母与符号的集合. 字符编码:​把 字符集 中的字符 编码为(映射)指定集合中的某一对象. # 以便文本在计算机中存储和通过通信网络的传递 查看文件的的编码格式 ...

  3. Unity UGUI的PointerEventData的介绍及使用

    Unity UGUI的PointerEventData的介绍及使用 1. 什么是PointerEventData? PointerEventData是Unity中UGUI系统中的一个重要组件,用于处理 ...

  4. linux-服务操作和运行级别和关机重启

    服务操作: service  network   [] systemctl     [ disable(禁用)  enable(启用)]     network [] 中为操作命令 : 1.statu ...

  5. ubuntu下安装mysqlclient报错

    输入以下代码: 1 解决方法: 2 sudo apt-get install libmysqlclient-dev 3 4 再次安装: 5 pip3 install mysqlclient 文章链接: ...

  6. python=2.7-not available from current channels

    现象 使用miniconda3创建python2的环境 Collecting package metadata (current_repodata.json): done Solving enviro ...

  7. Python 潮流周刊#15:如何分析 FastAPI 异步请求的性能?

    你好,我是猫哥.这里每周分享优质的 Python.AI 及通用技术内容,大部分为英文.标题取自其中一则分享,不代表全部内容都是该主题,特此声明. 本周刊精心筛选国内外的 250+ 信息源,为你挑选最值 ...

  8. 数仓备份经验分享丨详解roach备份原理及问题处理套路

    本文分享自华为云社区<GaussDB(DWS) 备份问题定位思路>,作者: yd_216390446. 前言 在数据库系统中,故障分为事务内部故障.系统故障.介质(磁盘)故障.对于事务内部 ...

  9. vue3 组合式 api 单文件组件写法

    1 Vue3 组合式 API 的基本原理和理念 1.1 Vue3 中的 Composition API Vue3 中的 Composition API 是一种新的编写组件逻辑的方式,它提供了更好的代码 ...

  10. P8810 [蓝桥杯 2022 国 C] 数组个数 题解

    思路比较简单的一道题. 用的五维 dp,看到二维和三维的 dp 直接膜了 orz. 正文开始. 分析 不难看出 dp. 因为 \(b_i\) 的值只与 \(a_{i-1},a_i,a_{i+1}\) ...