• 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 failed;
    6. public NaiveSynchronizationContext(Action completed, Action 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(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. ///
    16. /// 发布者
    17. ///
    18. public class Camera
    19. {
    20. public event EventHandler 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. ///
    65. /// 订阅者
    66. ///
    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. }

    结果输出:

     

  • 相关阅读:
    从零上手双指针
    unity面试题(基础篇)
    Linux文件管理知识:文本处理
    我们感知的内容是由大脑最支持的假设决定的吗?
    漫谈测试成长之探索——测试用例评审
    Vue基础语法【上】
    【655. 输出二叉树】
    研发团队数字化转型实践
    拥抱 Spring 全新 OAuth 解决方案
    【python】18行代码带你采集国外网小姐姐绝美图片
  • 原文地址:https://blog.csdn.net/qq_44386034/article/details/131791687