C# 深入理解事件(event)机制

目录

一,引言

二,事件的定义和用法

2.1 同步事件执行 

2.2 异步事件执行

2.3 等待异步事件完成

2.4 捕获异常处理中的异常

三,事件的综合案例

3.1 需求:汽车出停车场时收费,开闸放行


一,引言

都知道事件的本质是一个多播委托(MulticastDelegate),但对于事件的机制和用法一直懵懵懂懂,本篇主要对此进行深入分析,首先要明确关于事件的疑惑:

  • Event 是同步还是异步执行的?(答:同步执行)

  • 如果是多个订阅,事件执行的顺序是什么?(答:串行执行)

  • 如果事件执行中发生异常,会发生什么事情?(答:如果一个订阅者(事件)发生异常。未执行的事件不会继续执行)

  • 事件支持异步执行吗?(答:支持)

  • 事件触发后,跨进程可以触发到吗?(答:可以)

二,事件的定义和用法

事件作为类的成员,一般是通过事件向其他类或对象通知发生的相关事情。 发送事件的类称为发布者,接收事件的类称为订阅者。

  • 发布者确定何时引发事件;订阅者确定对事件作出何种响应

  • 一个事件可以有多个订阅者。 订阅者可以处理来自多个发行者的多个事件。

  • 没有订阅者的事件永远也不会引发。

  • 事件通常用于表示用户操作,例如单击按钮或图形用户界面中的菜单选项。

  • 当事件具有多个订阅者时,引发该事件时会同步调用事件处理程序。 也可通过async/await达到异步调用事件的作用。

  • 在 .NET 类库中,事件基于 EventHandler 委托和 EventArgs 基类。

2.1 同步事件执行 

定义一个Demo类,其内部有个事件是 DemoEvent,我们给他开放了一个接口Raise,如果谁敢调用它,那么,它就触发报警事件DemoEvent

        public class Demo{public event EventHandler DemoEvent;public void Raise(){try{this.DemoEvent?.Invoke(this, EventArgs.Empty);Console.WriteLine("所有的事件处理已经被执行!");}catch (Exception ex){}}}

随后在主程序中对事件进行订阅(这里采用了匿名方法进行订阅):

 static void Main(string[] args){var instance = new Demo();instance.DemoEvent += (sender, args) =>{Console.WriteLine("执行事件1!");};instance.DemoEvent += (sender, args) =>{Console.WriteLine("执行事件2!");};Console.WriteLine("*开始发起事件!");instance.Raise();Console.WriteLine("*事件执行完毕,继续下一项工作!");Console.ReadLine();}

输出结果:

可以看到,事件是一次同步执行的(执行过程也会阻塞主线程)。

2.2 异步事件执行

在上面代码基础上,增加异步方法然后订阅:

 结果输出:

可以看的,新增加的异步事件处理,的确是第一个被触发的,只不过它没有阻塞主线程处理。

小知识点:

  • 在异步编程中虽然不推崇定义一个类似的async void xxxx(){}函数,因为这样的函数无法被主程序捕获结果或异常。 但凡是总有例外,而这个异步事件处理恰恰就是这个函数的最佳使用场景。
  • 上述代码是非UI编程,有关UI处理(按钮点击事件等),机制并不一样,UI为它的异步事件提供了一个SynchronizationContext,使它们能够在UI线程上恢复。它从不“等待”事件。

2.3 等待异步事件完成

虽然2.2完成了异步事件的执行,但是在上面的输出结果中,存在一个问题:

*开始发起事件!
异步事件1执行开始
执行事件1!
执行事件2!
所有的事件处理已经被执行!
*事件执行完毕,继续下一项工作!
异步事件1执行完毕

[异步事件1执行完毕]应该在[*事件执行完毕,继续下一项工作!]前面输出才符合逻辑。但是异步执行的事件是不阻塞主线程的,那么如何让主线程等待异步事件的完成呢

这就涉及到异步编程async/await内部机制的问题了,因此我们需要引入SynchronizationContext的内容,自定义一个继承类,来实现相关的操作:

        public class Demo{public event EventHandler DemoEvent;public void Raise(){try{//3修改Raise函数,让事件的触发处在我们自定义的同步上下文内。this.DemoEvent?.NaiveRaiseAsync(this, EventArgs.Empty).GetAwaiter().GetResult();Console.WriteLine("所有的事件处理已经被执行!");}catch (Exception ex){Console.WriteLine("事件处理中发生异常!", ex.Message);}}}//主程序调用static void Main(string[] args){var instance = new Demo();//采用匿名订阅异步事件instance.DemoEvent += async (sendr, args) =>{Console.WriteLine("异步事件1执行开始");await Task.Delay(10);Console.WriteLine("异步事件1执行结果");};//传统的订阅异步事件instance.DemoEvent += method2;instance.DemoEvent += (sender, args) =>{Console.WriteLine("执行事件1!");};instance.DemoEvent += (sender, args) =>{Console.WriteLine("执行事件2!");};Console.WriteLine("*开始发起事件!");instance.Raise();Console.WriteLine("*事件执行完毕,继续下一项工作!");Console.ReadLine();}//异步方法static async void method2(object sender, EventArgs e){Console.WriteLine("异步事件2执行开始");await Task.Delay(100);Console.WriteLine("异步事件2执行完毕");}//1实现同步上下文(对异步的分裂点进行标记)public class NaiveSynchronizationContext:SynchronizationContext{private readonly Action completed;public NaiveSynchronizationContext(Action completed){this.completed = completed;}public override SynchronizationContext CreateCopy(){return new NaiveSynchronizationContext(this.completed);}public override void OperationStarted(){Console.WriteLine("同步上下文: 开始");}public override void OperationCompleted(){Console.WriteLine("同步上下文: 完成");this.completed();}}}//2对NaiveExtension函数进行扩展public static class NaiveExtension{public static Task NaiveRaiseAsync(this EventHandler @this, object sender, EventArgs eventArgs){// 如果没有事件处理,那么立即结束if (@this == null){return Task.CompletedTask;}var delegates = @this.GetInvocationList();var count = delegates.Length;var tcs = new TaskCompletionSource();foreach (var @delegate in @this.GetInvocationList()){// 检查AsyncStateMachineAttribute属性,判断是否异步处理函数var async = @delegate.Method.GetCustomAttributes(typeof(AsyncStateMachineAttribute), false).Any();// 定义 'completed' actionvar completed = new Action(() =>{if (Interlocked.Decrement(ref count) == 0){tcs.SetResult(true);}});if (async){SynchronizationContext.SetSynchronizationContext(new NaiveSynchronizationContext(completed));}@delegate.DynamicInvoke(sender, eventArgs);if (!async){// 如果不是异步,手工调用完成completed();}}return tcs.Task;}}

订阅了两个异步事件,两个同步事件,结果如下:

2.4 捕获异常处理中的异常

我们知道,在事件执行过程中,如果某个事件发生异常,就会终止未执行的事件:

 这里的原因是:

在基本synchronnizationcontext类中,Send和Post方法是使用应用程序ThreadPool实现的。因此,在事件处理程序中抛出的异常,实际上在打印上述消息的ThreadPool线程中抛出。

那么我们可以尝试重载 Post和Send看看。

    //1实现同步上下文(对异步的分裂点进行标记)public class NaiveSynchronizationContext : SynchronizationContext{private readonly Action completed;private readonly Action failed;public NaiveSynchronizationContext(Action completed, Action failed){this.completed = completed;this.failed = failed;}public override void Post(SendOrPostCallback d, object state){if (state is ExceptionDispatchInfo edi){Console.WriteLine("正捕获异常");this.failed(edi.SourceException);}else{Console.WriteLine("Posting");base.Post(d, state);}}public override void Send(SendOrPostCallback d, object state){if (state is ExceptionDispatchInfo edi){Console.WriteLine("正捕获异常");this.failed(edi.SourceException);}else{Console.WriteLine("Sending");base.Send(d, state);}}public override SynchronizationContext CreateCopy(){return new NaiveSynchronizationContext(this.completed, this.failed);}public override void OperationStarted(){Console.WriteLine("同步上下文: 开始");}public override void OperationCompleted(){Console.WriteLine("同步上下文: 完成");this.completed();}}//2对NaiveExtension函数进行扩展public static class NaiveExtension{public static Task NaiveRaiseAsync(this EventHandler @this, object sender, EventArgs eventArgs){// 如果没有事件处理,那么立即结束if (@this == null){return Task.CompletedTask;}var delegates = @this.GetInvocationList();var count = delegates.Length;var tcs = new TaskCompletionSource();var exception = (Exception)null;foreach (var @delegate in @this.GetInvocationList()){// 检查AsyncStateMachineAttribute属性,判断是否异步处理函数var async = @delegate.Method.GetCustomAttributes(typeof(AsyncStateMachineAttribute), false).Any();// 定义 'completed' actionvar completed = new Action(() =>{if (Interlocked.Decrement(ref count) == 0){if (exception is null){tcs.SetResult(true);}else{tcs.SetException(exception);}}});var failed = new Action(e =>{Interlocked.CompareExchange(ref exception, e, null);});if (async){SynchronizationContext.SetSynchronizationContext(new NaiveSynchronizationContext(completed, failed));}try{@delegate.DynamicInvoke(sender, eventArgs);}catch (TargetInvocationException e)when (e.InnerException != null){failed(e.InnerException);}catch (Exception e){failed(e);}if (!async){// 如果不是异步,手工调用完成completed();}}return tcs.Task;}}

最终输出结果:

可以看到的,这里的实现剔除了短路行为,即使你的某个处理函数有异常,它依然可以向下分发事件。 

三,事件的综合案例

3.1 需求:汽车出停车场时收费,开闸放行

通过分析需求,可以明确通过线程模拟相机实时抓拍车牌(发布者),当抓拍到车牌说明来车了,即触发一下事件:收费员收费,闸门放行(订阅者),具体代码如下:

  public class CarInfo : EventArgs{//停车的开始时间public DateTime StartTime { get; set; }//停车的结束时间public DateTime EndTime { get; set; }//车牌public string LicensePlate { get; set; }}public class SnapInfo{//车牌标志,拍到车牌说明有车;反之无车public string LicensePlate { get; set; }}/// /// 发布者/// public class Camera{public event EventHandler OnSnapLicenseEvent;//模拟摄像机循环在抓拍车牌public void SnapPhoto(){Task.Run(() =>{List license = new List(){ "","","","沪A11111", "沪B22222","沪C33333","","" };Random random = new Random();while(true){Thread.Sleep(1000);int index = random.Next(1, license.Count + 1);SnapInfo snapInfo = new SnapInfo() { LicensePlate = license[index - 1] };//当车牌不为空的时候表示车来了if (!string.IsNullOrEmpty(snapInfo.LicensePlate)){Console.WriteLine($"抓拍到车牌{snapInfo.LicensePlate}!");OnSnapLicense(GetCarInfoBySnapInfo(snapInfo));}else{Console.WriteLine("当前没有抓拍到车牌!");Console.WriteLine("--------------------------------------");}}});}public CarInfo GetCarInfoBySnapInfo(SnapInfo snapInfo){//抓拍到车牌后,这里直接赋值,相当于模拟通过接口车牌查询了该车的进场数据CarInfo carInfo = new CarInfo(){StartTime = DateTime.Parse("2023-08-03 12:00:00"),EndTime = DateTime.Now,LicensePlate = snapInfo.LicensePlate,};return carInfo;}public void OnSnapLicense(CarInfo carInfo){OnSnapLicenseEvent?.Invoke(this, carInfo);}}
/// 
/// 订阅者
/// //收费员(负责收费)public class Charger{//收费public void Charge(object sender,CarInfo carInfo){Console.WriteLine($"收费员:对{carInfo.LicensePlate}完成了收费");}}//闸机(负责开关)public class Gate{public void OpenGate(object sender,CarInfo carInfo){Console.WriteLine($"闸机对:{carInfo.LicensePlate}车辆放行");}}class Program{static void Main(string[] args){//先分析需求:车到车库门口,摄像机要拍照到车牌后,收费员收费,闸机抬杆Camera camera = new Camera();Charger charger = new Charger();Gate gate = new Gate();camera.OnSnapLicenseEvent += charger.Charge;camera.OnSnapLicenseEvent += gate.OpenGate;camera.SnapPhoto();Console.ReadLine();}}

结果输出:

 


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部