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

目录

一,引言

二,事件的定义和用法

[2.1 同步事件执行](#2.1 同步事件执行)

[2.2 异步事件执行](#2.2 异步事件执行)

[2.3 等待异步事件完成](#2.3 等待异步事件完成)

[2.4 捕获异常处理中的异常](#2.4 捕获异常处理中的异常)


一,引言

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

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

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

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

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

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

二,事件的定义和用法

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

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

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

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

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

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

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

2.1 同步事件执行

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

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

                }
            }
        }

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

cs 复制代码
 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完成了异步事件的执行,但是在上面的输出结果中,存在一个问题:

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

异步事件1执行完毕\]应该在\[\*事件执行完毕,继续下一项工作!\]前面输出才符合逻辑。但是异步执行的事件是不阻塞主线程的,那么**如何让主线程等待异步事件的完成呢**? 这就涉及到异步编程async/await内部机制的问题了,因此我们需要引入**SynchronizationContext**的内容,自定义一个继承类,来实现相关的操作: ```cs 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' action var 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; } } ``` 订阅了两个异步事件,两个同步事件,结果如下: ![](https://file.jishuzhan.net/article/1682544532903694337/fc9e1e95ef3f47dd91799504687aa045.png) ## 2.4 捕获异常处理中的异常 我们知道,在事件执行过程中,如果某个事件发生异常,就会终止未执行的事件: ![](https://file.jishuzhan.net/article/1682544532903694337/310d2e7df36047d9bf1a0fd6da02bf4c.png) 这里的原因是: > 在基本`synchronnizationcontext`类中,Send和Post方法是使用应用程序`ThreadPool`实现的。因此,在事件处理程序中抛出的异常,实际上在打印上述消息的`ThreadPool`线程中抛出。 那么我们可以尝试重载 Post和Send看看。 ```cs //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' action var 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; } } ``` 最终输出结果: ![](https://file.jishuzhan.net/article/1682544532903694337/43f1eab7e19743fc953781cfd0d206ef.png) 可以看到的,这里的实现剔除了短路行为,即使你的某个处理函数有异常,它依然可以向下分发事件。

相关推荐
月之圣痕10 分钟前
c#清理释放内存
c#
wt_cs21 分钟前
身份认证C#集成方案-数字时代身份证实名认证利器
开发语言·c#
幼儿园园霸柒柒2 小时前
第七章:7.2求方程a*x*x+b*x+c=0的根,用3个函数,分别求当:b*b-4*a*c大于0、等于0和小于0时的根并输出结果。从主函数输入a、b、c的值
c语言·开发语言·算法·c#
归海_一刀6 小时前
【Unity动态换装骨骼合并】
unity·c#·游戏引擎
我转的头好晕9 小时前
EF Core基本使用
数据库·c#·asp.net
GSDjisidi9 小时前
日本IT|车载C#开发工程师的前途及职业发展
开发语言·c#
进阶的小木桩9 小时前
VSTO幻灯片退出播放(C#模拟键盘鼠标的事件)
开发语言·c#·计算机外设
Y咚咚10 小时前
winform WebSockets连接服务端
c#
lljss202019 小时前
C# JSON
c#