当前位置:   article > 正文

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

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

  1. public class Demo
  2. {
  3. public event EventHandler DemoEvent;
  4. public void Raise()
  5. {
  6. try
  7. {
  8. this.DemoEvent?.Invoke(this, EventArgs.Empty);
  9. Console.WriteLine("所有的事件处理已经被执行!");
  10. }
  11. catch (Exception ex)
  12. {
  13. }
  14. }
  15. }

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

  1. static void Main(string[] args)
  2. {
  3. var instance = new Demo();
  4. instance.DemoEvent += (sender, args) =>
  5. {
  6. Console.WriteLine("执行事件1!");
  7. };
  8. instance.DemoEvent += (sender, args) =>
  9. {
  10. Console.WriteLine("执行事件2!");
  11. };
  12. Console.WriteLine("*开始发起事件!");
  13. instance.Raise();
  14. Console.WriteLine("*事件执行完毕,继续下一项工作!");
  15. Console.ReadLine();
  16. }

输出结果:

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

2.2 异步事件执行

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

 结果输出:

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

小知识点:

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

2.3 等待异步事件完成

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

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

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

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

  1. public class Demo
  2. {
  3. public event EventHandler DemoEvent;
  4. public void Raise()
  5. {
  6. try
  7. {
  8. //3修改Raise函数,让事件的触发处在我们自定义的同步上下文内。
  9. this.DemoEvent?.NaiveRaiseAsync(this, EventArgs.Empty).GetAwaiter().GetResult();
  10. Console.WriteLine("所有的事件处理已经被执行!");
  11. }
  12. catch (Exception ex)
  13. {
  14. Console.WriteLine("事件处理中发生异常!", ex.Message);
  15. }
  16. }
  17. }
  18. //主程序调用
  19. static void Main(string[] args)
  20. {
  21. var instance = new Demo();
  22. //采用匿名订阅异步事件
  23. instance.DemoEvent += async (sendr, args) =>
  24. {
  25. Console.WriteLine("异步事件1执行开始");
  26. await Task.Delay(10);
  27. Console.WriteLine("异步事件1执行结果");
  28. };
  29. //传统的订阅异步事件
  30. instance.DemoEvent += method2;
  31. instance.DemoEvent += (sender, args) =>
  32. {
  33. Console.WriteLine("执行事件1!");
  34. };
  35. instance.DemoEvent += (sender, args) =>
  36. {
  37. Console.WriteLine("执行事件2!");
  38. };
  39. Console.WriteLine("*开始发起事件!");
  40. instance.Raise();
  41. Console.WriteLine("*事件执行完毕,继续下一项工作!");
  42. Console.ReadLine();
  43. }
  44. //异步方法
  45. static async void method2(object sender, EventArgs e)
  46. {
  47. Console.WriteLine("异步事件2执行开始");
  48. await Task.Delay(100);
  49. Console.WriteLine("异步事件2执行完毕");
  50. }
  51. //1实现同步上下文(对异步的分裂点进行标记)
  52. public class NaiveSynchronizationContext:SynchronizationContext
  53. {
  54. private readonly Action completed;
  55. public NaiveSynchronizationContext(Action completed)
  56. {
  57. this.completed = completed;
  58. }
  59. public override SynchronizationContext CreateCopy()
  60. {
  61. return new NaiveSynchronizationContext(this.completed);
  62. }
  63. public override void OperationStarted()
  64. {
  65. Console.WriteLine("同步上下文: 开始");
  66. }
  67. public override void OperationCompleted()
  68. {
  69. Console.WriteLine("同步上下文: 完成");
  70. this.completed();
  71. }
  72. }
  73. }
  74. //2对NaiveExtension函数进行扩展
  75. public static class NaiveExtension
  76. {
  77. public static Task NaiveRaiseAsync(this EventHandler @this, object sender, EventArgs eventArgs)
  78. {
  79. // 如果没有事件处理,那么立即结束
  80. if (@this == null)
  81. {
  82. return Task.CompletedTask;
  83. }
  84. var delegates = @this.GetInvocationList();
  85. var count = delegates.Length;
  86. var tcs = new TaskCompletionSource<bool>();
  87. foreach (var @delegate in @this.GetInvocationList())
  88. {
  89. // 检查AsyncStateMachineAttribute属性,判断是否异步处理函数
  90. var async = @delegate.Method.GetCustomAttributes(typeof(AsyncStateMachineAttribute), false).Any();
  91. // 定义 'completed' action
  92. var completed = new Action(() =>
  93. {
  94. if (Interlocked.Decrement(ref count) == 0)
  95. {
  96. tcs.SetResult(true);
  97. }
  98. });
  99. if (async)
  100. {
  101. SynchronizationContext.SetSynchronizationContext(new NaiveSynchronizationContext(completed));
  102. }
  103. @delegate.DynamicInvoke(sender, eventArgs);
  104. if (!async)
  105. {
  106. // 如果不是异步,手工调用完成
  107. completed();
  108. }
  109. }
  110. return tcs.Task;
  111. }
  112. }

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

2.4 捕获异常处理中的异常

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

 这里的原因是:

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

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

  1. //1实现同步上下文(对异步的分裂点进行标记)
  2. public class NaiveSynchronizationContext : SynchronizationContext
  3. {
  4. private readonly Action completed;
  5. private readonly Action<Exception> failed;
  6. public NaiveSynchronizationContext(Action completed, Action<Exception> failed)
  7. {
  8. this.completed = completed;
  9. this.failed = failed;
  10. }
  11. public override void Post(SendOrPostCallback d, object state)
  12. {
  13. if (state is ExceptionDispatchInfo edi)
  14. {
  15. Console.WriteLine("正捕获异常");
  16. this.failed(edi.SourceException);
  17. }
  18. else
  19. {
  20. Console.WriteLine("Posting");
  21. base.Post(d, state);
  22. }
  23. }
  24. public override void Send(SendOrPostCallback d, object state)
  25. {
  26. if (state is ExceptionDispatchInfo edi)
  27. {
  28. Console.WriteLine("正捕获异常");
  29. this.failed(edi.SourceException);
  30. }
  31. else
  32. {
  33. Console.WriteLine("Sending");
  34. base.Send(d, state);
  35. }
  36. }
  37. public override SynchronizationContext CreateCopy()
  38. {
  39. return new NaiveSynchronizationContext(this.completed, this.failed);
  40. }
  41. public override void OperationStarted()
  42. {
  43. Console.WriteLine("同步上下文: 开始");
  44. }
  45. public override void OperationCompleted()
  46. {
  47. Console.WriteLine("同步上下文: 完成");
  48. this.completed();
  49. }
  50. }
  51. //2对NaiveExtension函数进行扩展
  52. public static class NaiveExtension
  53. {
  54. public static Task NaiveRaiseAsync(this EventHandler @this, object sender, EventArgs eventArgs)
  55. {
  56. // 如果没有事件处理,那么立即结束
  57. if (@this == null)
  58. {
  59. return Task.CompletedTask;
  60. }
  61. var delegates = @this.GetInvocationList();
  62. var count = delegates.Length;
  63. var tcs = new TaskCompletionSource<bool>();
  64. var exception = (Exception)null;
  65. foreach (var @delegate in @this.GetInvocationList())
  66. {
  67. // 检查AsyncStateMachineAttribute属性,判断是否异步处理函数
  68. var async = @delegate.Method.GetCustomAttributes(typeof(AsyncStateMachineAttribute), false).Any();
  69. // 定义 'completed' action
  70. var completed = new Action(() =>
  71. {
  72. if (Interlocked.Decrement(ref count) == 0)
  73. {
  74. if (exception is null)
  75. {
  76. tcs.SetResult(true);
  77. }
  78. else
  79. {
  80. tcs.SetException(exception);
  81. }
  82. }
  83. });
  84. var failed = new Action<Exception>(e =>
  85. {
  86. Interlocked.CompareExchange(ref exception, e, null);
  87. });
  88. if (async)
  89. {
  90. SynchronizationContext.SetSynchronizationContext(new NaiveSynchronizationContext(completed, failed));
  91. }
  92. try
  93. {
  94. @delegate.DynamicInvoke(sender, eventArgs);
  95. }
  96. catch (TargetInvocationException e)
  97. when (e.InnerException != null)
  98. {
  99. failed(e.InnerException);
  100. }
  101. catch (Exception e)
  102. {
  103. failed(e);
  104. }
  105. if (!async)
  106. {
  107. // 如果不是异步,手工调用完成
  108. completed();
  109. }
  110. }
  111. return tcs.Task;
  112. }
  113. }

最终输出结果:

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

三,事件的综合案例

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

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

  1. public class CarInfo : EventArgs
  2. {
  3. //停车的开始时间
  4. public DateTime StartTime { get; set; }
  5. //停车的结束时间
  6. public DateTime EndTime { get; set; }
  7. //车牌
  8. public string LicensePlate { get; set; }
  9. }
  10. public class SnapInfo
  11. {
  12. //车牌标志,拍到车牌说明有车;反之无车
  13. public string LicensePlate { get; set; }
  14. }
  15. /// <summary>
  16. /// 发布者
  17. /// </summary>
  18. public class Camera
  19. {
  20. public event EventHandler<CarInfo> OnSnapLicenseEvent;
  21. //模拟摄像机循环在抓拍车牌
  22. public void SnapPhoto()
  23. {
  24. Task.Run(() =>
  25. {
  26. List<string> license = new List<string>()
  27. { "","","","沪A11111", "沪B22222","沪C33333","","" };
  28. Random random = new Random();
  29. while(true)
  30. {
  31. Thread.Sleep(1000);
  32. int index = random.Next(1, license.Count + 1);
  33. SnapInfo snapInfo = new SnapInfo() { LicensePlate = license[index - 1] };
  34. //当车牌不为空的时候表示车来了
  35. if (!string.IsNullOrEmpty(snapInfo.LicensePlate))
  36. {
  37. Console.WriteLine($"抓拍到车牌{snapInfo.LicensePlate}!");
  38. OnSnapLicense(GetCarInfoBySnapInfo(snapInfo));
  39. }
  40. else
  41. {
  42. Console.WriteLine("当前没有抓拍到车牌!");
  43. Console.WriteLine("--------------------------------------");
  44. }
  45. }
  46. });
  47. }
  48. public CarInfo GetCarInfoBySnapInfo(SnapInfo snapInfo)
  49. {
  50. //抓拍到车牌后,这里直接赋值,相当于模拟通过接口车牌查询了该车的进场数据
  51. CarInfo carInfo = new CarInfo()
  52. {
  53. StartTime = DateTime.Parse("2023-08-03 12:00:00"),
  54. EndTime = DateTime.Now,
  55. LicensePlate = snapInfo.LicensePlate,
  56. };
  57. return carInfo;
  58. }
  59. public void OnSnapLicense(CarInfo carInfo)
  60. {
  61. OnSnapLicenseEvent?.Invoke(this, carInfo);
  62. }
  63. }
  64. /// <summary>
  65. /// 订阅者
  66. /// </summary>
  67. //收费员(负责收费)
  68. public class Charger
  69. {
  70. //收费
  71. public void Charge(object sender,CarInfo carInfo)
  72. {
  73. Console.WriteLine($"收费员:对{carInfo.LicensePlate}完成了收费");
  74. }
  75. }
  76. //闸机(负责开关)
  77. public class Gate
  78. {
  79. public void OpenGate(object sender,CarInfo carInfo)
  80. {
  81. Console.WriteLine($"闸机对:{carInfo.LicensePlate}车辆放行");
  82. }
  83. }
  84. class Program
  85. {
  86. static void Main(string[] args)
  87. {
  88. //先分析需求:车到车库门口,摄像机要拍照到车牌后,收费员收费,闸机抬杆
  89. Camera camera = new Camera();
  90. Charger charger = new Charger();
  91. Gate gate = new Gate();
  92. camera.OnSnapLicenseEvent += charger.Charge;
  93. camera.OnSnapLicenseEvent += gate.OpenGate;
  94. camera.SnapPhoto();
  95. Console.ReadLine();
  96. }
  97. }

结果输出:

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/984203
推荐阅读
  

闽ICP备14008679号