百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

搞懂事件——C# 的event的机制深度理解

bigegpt 2024-09-22 00:45 3 浏览

  • ??欢迎点赞 :?? 收藏 ?留言 ?? 如有错误敬请指正,赐人玫瑰,手留余香!

  • ??本文作者:由webmote 原创

  • ??作者格言:无尽的折腾后,终于又回到了起点,工控,我来了 !

1. 前言


为什么忽然对Event感兴趣了?

因为进入Web时代以后,很少使用它了,忽然想起这个知识点,感到非常的熟悉而陌生。

不知道你有没有类似的感觉:好像对某个点大脑很熟悉,而手又很陌生?就像多年未曾谋面的老朋友,一个瞬间涌入大脑很多往日嬉戏打闹的碎片,但念及当先,却又音信全无。

那么,你有没有事件相关的疑惑呢?譬如:

  • Event 是同步还是异步执行的?

  • 如果是多个订阅,事件执行的顺序是什么?

  • 如果事件执行中发生异常,会发生什么事情?

  • 事件支持异步执行吗?

  • 事件触发后,跨进程可以触发到吗?

  • 事件总线/领域事件 为啥不使用event实现呢?

如果你也有类似的疑惑,那么不妨和我探究一番。

2. 定义和特性

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

在典型的C# Windows 窗体或Web应用程序中,可订阅由按钮和列表框等控件引发的事件。 这也是我们觉得熟悉的原因吧?

MSDN中对事件有如下界定

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

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

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

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

  • 当事件具有多个订阅者时,引发该事件时会同步调用事件处理程序。 若要异步调用事件,请参阅 “使用异步方式调用同步方法”。

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

从上述定义中,应该能解决是否同步的问题吧?这里再明确的说下:

  • 事件是同步的, 因此触发事件时,会被阻塞(如果订阅事件不是异步的)!

  • 如果是多个订阅,他们会一个个串行执行。

  • 如果其中一个订阅者抛出异常,尚未执行的订阅者将不会执行。

哎,什么,看的更糊涂了?

是啊,怎么又引入了订阅事件可以是异步的概念?

别急,都是理论会让人蒙的,我们来看看正常的例子吧!

3. 关于事件的一个小例子

以下例子均在**.net6** 环境下进行,望知悉。

namespace Event1
{
internal class Program
{
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("*事件执行完毕,继续下一项工作!");
}
}

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

}

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

这里模拟了2个订阅者,分别处理报警事件DemoEvent
程序执行的结果是什么呢?你有没有猜对?

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

嗯,多次运行,仍然是此结果! 可见事件的确是顺序执行的,并且其为同步执行。

现在,我们加入异常,再看看:

instance.DemoEvent += (sender, args) =>
{
Console.WriteLine("执行事件1!");
throw new Exception("执行事件1,错误");
};

## 结果如下 ##
*开始发起事件!
执行事件1!
事件处理中发生异常!
*事件执行完毕,继续下一项工作!

可见,如果你想让每个订阅者都可以好好执行处理的话,那每个订阅者在订阅程序内,必须自己处理好程序异常,不要抛出来哦!

另外,注意一点,如果程序需要保持稳健,那么你还需要考虑取消订阅,以便防止内存泄漏哦!

instance.DemoEvent += Instance_DemoEvent;
instance.DemoEvent-= Instance_DemoEvent;

4. 事件的异步处理

上面提到的有关事件的异步处理,这又是一个什么鬼东西呢?

先说明下,这里的例子都是基于.net6的非UI编程,有关UI处理按钮点击事件等,机制并不一样,它们的内部做了处理,因此ui处理异步事件的方式与这里并不同。UI为它的异步事件提供了一个SynchronizationContext,使它们能够在UI线程上恢复。它从不“等待”事件。切记切记。

我们在订阅者1前面再增加一个异步订阅者

 instance.DemoEvent += async (sender, args) =>
{
Console.WriteLine("执行事件1开始??");
await Task.Delay(10);
Console.WriteLine("执行事件1结束??");
};
// 为了等待这个慢家伙,需要在事件执行完毕的后面增加一行代码,让主程序等会退出。
Console.ReadLine();

现在执行以下,看看谁是第一呢?

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

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

一个小知识点, 我们以前都不推崇定义一个类似的async void xxxx(){}函数,因为这样的函数无法被主程序捕获结果或异常。 但凡是总有例外,而这个异步事件处理恰恰就是这个函数的最佳使用场景。

5. 等待所有异步处理订阅者

眼尖的朋友们,应该在上面的结果输出中,看到了一个不和谐的信息。
没有看到的朋友,该去检查眼睛了。

所有的事件处理已经被执行!
*事件执行完毕,继续下一项工作!
执行事件1结束??

这个异步事件还没执行完,就打印出来了所有的事件处理已经被执行!的信息了。

嗯,是有这种应用场景,反正只要触发了处理就行,什么时候处理完,那都是订阅者的事情。

当然,也有一种场景,是需要等待所有的订阅者处理完消息,有异步的,也有同步的。

如果是后一种情景的话,那么我们还有什么办法呢?

这就涉及到async和await内部机制的问题了,有关知识,在这里不赘述。这里只将实现思路。

我们需要引入 SynchronizationContext的内容,自定义一个继承类,来实现相关操作。

如下,我们先实现一个天真无邪的同步上下文类。

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();
}
}

为了方便使用,我们再定义一个扩展函数

public static class NaiveExtension
{
public static Task NaiveRaiseAsync( this EventHandler @this, object sender, EventArgs eventArgs)
{
// 如果没有事件处理,那么立即结束
if (@this == )
{
return Task.CompletedTask;
}
var tcs = new TaskCompletionSource<bool>();
var sc = new NaiveSynchronizationContext(() => tcs.SetResult(true));

SynchronizationContext.SetSynchronizationContext(sc);

@this.Invoke(sender, eventArgs);

return tcs.Task;
}
}

真正的使用,需要修改Raise函数,让事件的触发处在我们自定义的同步上下文内。

public void Raise()
{
try
{
this.DemoEvent?.NaiveRaiseAsync(this,EventArgs.Empty).GetAwaiter().GetResult();
//this.DemoEvent?.Invoke(this, EventArgs.Empty);
Console.WriteLine("所有的事件处理已经被执行!");
}
catch(Exception ex)
{
Console.WriteLine("事件处理中发生异常!", ex.Message);
}
}

嗯,感觉大功告成了,我们再来看看结果:

*开始发起事件!
同步上下文: 开始
执行事件1开始??
执行事件1!
执行事件2!
执行事件1结束??
同步上下文: 完成
所有的事件处理已经被执行!
*事件执行完毕,继续下一项工作!

哈哈,开心啊,搞定了这个小小需求!
来,多加几个异步处理试试!

什么,有异常了?

System.InvalidOperationException:
“An attempt was made to transition a task to a final state when it had already completed.”

仔细阅读异常,原来是TaskCompletionSource.SetResult被太早的调用所致,正确的做法需要等待所有异步处理完成后,再进行调用,当然需要修正下同步和异步的不同之处。
那就好办了,我们引入Interlocked类,计算跟踪每个异步处理就可以了。

public static Task NaiveRaiseAsync( this EventHandler @this, object sender, EventArgs eventArgs)
{
// 如果没有事件处理,那么立即结束
if (@this == )
{
return Task.CompletedTask;
}
var delegates = @this.GetInvocationList();
var count = delegates.Length;

var tcs = new TaskCompletionSource<bool>();
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;
}

再次执行结果,啊哈哈,看吧:

*开始发起事件!
同步上下文: 开始
执行事件1开始??
同步上下文: 开始
执行事件2开始??
执行事件1!
执行事件2!
执行事件2结束??
执行事件1结束??
同步上下文: 完成
同步上下文: 完成
所有的事件处理已经被执行!
*事件执行完毕,继续下一项工作!

6. 捕获异常处理中的异常

上面的处理已经非常好了,当然,我说的是正常逻辑,那么当我们在异常处理中引入异常,会发生什么呢?

说干就干,是我的风格,来吧,让暴风雨来的更猛烈些吧!

instance.DemoEvent += async (sender, args) =>
{
Console.WriteLine("执行事件1开始??");
throw new InvalidOperationException("Sabotage!");
await Task.Delay(10);
Console.WriteLine("执行事件1结束??");
};

虽然我们在触发事件时增加了异常捕获,但好像捕获了个寂寞!

*开始发起事件!
同步上下文: 开始
执行事件1开始??
同步上下文: 完成
同步上下文: 开始
Unhandled exception. 执行事件2开始??
执行事件1!
执行事件2!
执行事件2结束??
同步上下文: 完成
所有的事件处理已经被执行!
*事件执行完毕,继续下一项工作!
System.InvalidOperationException: Sabotage!

程序直接退出了。

这里的原因是:

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

那么我们试着重载 Post和Send看看。

public class NaiveSynchronizationContext : SynchronizationContext
{
private readonly Action completed;


private readonly Action<Exception> failed;

public NaiveSynchronizationContext(Action completed, Action<Exception> 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();
}
}
public static class NaiveExtension
{
public static Task NaiveRaiseAsync( this EventHandler @this, object sender, EventArgs eventArgs)
{
// 如果没有事件处理,那么立即结束
if (@this == )
{
return Task.CompletedTask;
}
var delegates = @this.GetInvocationList();
var count = delegates.Length;

var tcs = new TaskCompletionSource<bool>();
var exception = (Exception);
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 )
{
tcs.SetResult(true);
}
else
{
tcs.SetException(exception);
}
}
});
var failed = new Action<Exception>(e =>
{
Interlocked.CompareExchange( ref exception, e, );
});
if (async)
{
SynchronizationContext.SetSynchronizationContext(new NaiveSynchronizationContext(completed, failed));
}

ry
{
@delegate.DynamicInvoke(sender, eventArgs);
}
catch (TargetInvocationException e)
when (e.InnerException != )
{
failed(e.InnerException);
}
catch (Exception e)
{
failed(e);
}

if (!async)
{
// 如果不是异步,手工调用完成
completed();
}
}
return tcs.Task;
}
}

再次执行,看看是怎么样的?

*开始发起事件!
同步上下文: 开始
执行事件1开始??
正捕获异常
同步上下文: 完成
同步上下文: 开始
执行事件2开始??
执行事件1!
执行事件2!
Posting
执行事件2结束??
同步上下文: 完成
事件处理中发生异常!
*事件执行完毕,继续下一项工作!

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

7. 事件总线和领域事件

领域事件是领域专家所关心的(需要跟踪的、希望被通知的、会引起其他模型对象改变状态的)发生在领域中的一些事情。 简而言之,领域事件是用来捕获领域中发生的具有业务价值的一些事情。 它的本质就是事件,如果不牵涉到微服务和存储事件,我觉得你可以考虑使用event来实现它。

不过,由于大部分的领域事件可能都需要考虑存储或者跨服务的行为,因此我们很少看见类似的实现。

而事件总线总是和分布式应用或微服务联系在一起,因此跨进程成了刚需,在这个前提下,才不得不引入了类似Rabbitmq的消息服务器。

8. web中的应用

WEB中应用也是有的,只是需要找到合适的场景。

  • 进程内事件!

  • 不需要跨进程!


我在abp的框架里搜到了这个。

9. 小结

好文章值得你收藏,看了这么多,不知道你搞懂没有,我感觉我好像讲明白了…

??都收藏了,还在乎一个评论吗?


相关推荐

5分钟调色大片的方法(5分钟调色大片的方法有哪些)

哈喽大家好。在大家印象中一定觉得ps非常难学非常难。大家不要着急,小编的教学都是针对ps零基础的同学的,而且非常实用哦。只要大家跟着图文练习一两遍,保证大家立马学会~!好了,废话少说,下面开始我们今天...

闪白特效原来是这么用的(闪白特效怎么使用)

作者|高艳侠订阅|010-86092062闪白特效是影视作品中应用比较多的效果之一,那么具体该在哪些场景使用闪白特效?具体该如何操作?下面就以AdobePremiere(以下简称PR)为例,...

ppt常用小图标去哪里找?3个矢量素材网站推荐!

ppt是一个注重可视化表达的演示载体,除了高清图片,ppt中另一类常用的素材是各种小图标,也叫矢量图标,巧妙运用小图标能提升整体美观度和表现力,那么ppt常用小图标去哪里找呢?为方便各位快速找到合适的...

有什么好用的截图录屏工具?试试这9款

经常有朋友反馈苦于缺乏截屏和录屏的趁手工具,本期我们分享几个相当好用的截屏和录屏工具,希望能帮到大家。ScreenToGifScreenToGif是一款免费且开源的录屏工具。此款工具最大的特点是可以...

配色苦手福音!专业快速色环配色PS插件

今天橘子老师给的大家介绍的是一款快速配色的插件,非常强大配色苦手福音来啦!(获取方式见文末)【插件介绍】配色在后期设计中占有主导地位,好的配色能让作品更加抢眼Coolorus这款专业的配色插件,能够...

如何用PS抠主体?(ps怎么抠主体)

1.主体法抠图-抠花苞和花梗导入一张荷花苞的照片,点击上图中顶部“选择”菜单栏,下拉单击“主体”。可以看到,只有花苞被选中,但是花梗并没有被选中。接下来单击上图中左侧工具栏的“快速选择工具”,上图中顶...

2799元的4K电视,有保障吗?(买4k电视机哪个品牌好)

在上一期《电脑报》的3·15专题报道中,我们揭露了一款不靠谱的42英寸4K智能电视——TCLD42A561U。这款售价2699元的4K智能电视不仅4K画质方面存在严重问题,而且各种功能和应用体验也不理...

苹果电脑的Touch Bar推出一段时间了 这款工具可以帮你开发适用于它的APP

距离苹果推出带有TouchBar的MacBookPro已经有一段时间了,除了那些像Adobe、Google和Microsoft大公司在开发适用于TouchBar的应用之外,其实还有很多独立的开...

如魔法般吸取颜色的桌灯(如魔法般吸取颜色的桌灯叫什么)

色彩为生活带来的感官刺激,逐渐被视为理所当然。一盏桌灯运用它的神奇力量,将隐藏于物件中的颜色逐一释放,成为装点环境的空间魔法师。ColorUp是一款可以改变颜色的吸色台灯,沿用传统灯泡的造型,融入了拾...

一篇文章带你用jquery mobile设计颜色拾取器

【一、项目背景】现实生活中,我们经常会遇到配色的问题,这个时候去百度一下RGB表。而RGB表只提供相对于的颜色的RGB值而没有可以验证的模块。我们可以通过jquerymobile去设计颜色的拾取器...

ps拾色器快捷键是什么?(ps2019拾色器快捷键)

ps拾色器快捷键是什么?文章末尾有获取方式,按照以下步骤就能自动获得!学会制作PS特效需要一定程度的耐心和毅力。初学者可以从基本的工具和技术开始学习,逐渐提高他们的技能水平。同时,观看更多优秀的特效作...

免费开源的 Windows 截图录屏工具,支持 OCR 识别和滚动截图等

功能很强大、安装很小巧的免费截图、录屏工具,提供很多使用的工具来帮我么能解决问题,推荐给大家。关于ShareXShareX是一款免费的windows工具,起初是一个小巧的截图工具,经过多年的迭...

入门到精通系列PS教程:第13篇 · 拾色器、颜色问题说明及补充

入门到精通系列PS教程:第13篇·拾色器、颜色问题说明及补充作者|侯潇问题说明我的第12篇教程里,有个小问题没有说清楚。要说是错误,又不算是错误,只是没有说准确。写完那篇教程后,因为已经到了深...

PS冷知识:用吸管工具吸取屏幕上的任意颜色

今天,我们给大家介绍PS中的一个冷知识:用吸管工具可以吸取屏幕上的任意颜色。其实,操作起来是非常简单的。大多数情况下,我们认为,PS的吸管工具只能吸取PS软件作图区域范围内的颜色,最多加上画布四周的...

Windows 11 将提供内置颜色选择器工具

Windows11内置了颜色选择器,可以扫描并识别屏幕上的颜色并生成颜色代码。此外,微软还利用人工智能技术,让屏幕上的文本扫描和选择变得更加便捷。这两项功能均已在SnippingToolv1...