百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

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

toyiye 2024-07-09 22:44 10 浏览 0 评论

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

  • ??本文作者:由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. 小结

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

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


相关推荐

Python 可视化工具包(python常见的可视化工具)

喜欢用Python做项目的小伙伴不免会遇到这种情况:做图表时,用哪种好看又实用的可视化工具包呢?本文将介绍一些常用的Python可视化包,包括这些包的优缺点以及分别适用于什么样的场景。这篇文章...

Python的GPU编程实例——近邻表计算

目录技术背景...

python算法体验-3.python实现欧式距离的三种方式

欧式距离也称欧几里得距离,是最常见的距离度量,衡量的是多维空间中两个点之间的绝对距离。欧式距离源自N维欧氏空间中两点...

python实现Lasso回归分析(特征筛选、建模预测)

实现功能:...

python语言检测模块langid、langdetect使用

本文首发地址:https://blog.csdn.net/Together_CZ/article/details/86678423欢迎关注我的博客【Together_CZ】,我是沂水寒城!之前使用数据...

7天学会Python最佳可视化工具Seaborn(一):可视化变量间的关系

众所周知,Seaborn“可能”是Python下最友好、易用的可视化工具了,可视化效果也非常好。但是截止目前,并没有一份中文教程供广大国内Python使用者查阅学习。怎么能因为语言的问题,让大家错过这...

在Python中使用K-Means聚类和PCA主成分分析进行图像压缩

各位读者好,在这篇文章中我们尝试使用sklearn库比较k-means聚类算法和主成分分析(PCA)在图像压缩上的实现和结果。压缩图像的效果通过占用的减少比例以及和原始图像的差异大小来评估。图像压...

OpenCV-Python 相机校准 | 四十九

目标在本节中,我们将学习由相机引起的失真类型,如何找到相机的固有和非固有特性如何根据这些特性使图像不失真基础一些针孔相机会给图像带来明显的失真。两种主要的变形是径向变形和切向变形。径向变形会导致直线出...

python数据预处理技术(python 数据预处理)

在真实世界中,经常需要处理大量的原始数据,这些原始数据是机器学习算法无法理解的。为了让机器学习算法理解原始数据,需要对数据进行预处理。我们运行anaconda集成环境下的“jupyternotebo...

【Python可视化系列】一文教你绘制不同类型散点图(理论+源码)

这是...

OpenCV-Python 特征匹配 | 四十四

目标在本章中,我们将看到如何将一个图像中的特征与其他图像进行匹配。我们将在OpenCV中使用Brute-Force匹配器和FLANN匹配器Brute-Force匹配器的基础蛮力匹配器很简单。它使用第一...

实战python中Random模块使用(python中的random模块)

一、random模块简介Python标准库中的random函数,可以生成随机浮点数、整数、字符串,甚至帮助你随机选择列表序列中的一个元素,打乱一组数据等。要在Python中使用random模块,只需要...

Python随机模块22个函数详解(python随机函数的应用)

随机数可以用于数学,游戏,安全等领域中,还经常被嵌入到算法中,用以提高算法效率,并提高程序的安全性。平时数据分析各种分布的数据构造也会用到。random模块,用于生成伪随机数,之所以称之为伪随机数,是...

说冲A就冲A,这个宝藏男孩冯俊杰我pick了

爱奇艺新上架了一部网剧叫《最后一个女神》。有个惊人的发现,剧里男三居然是《青春有你》的训练生冯俊杰。剧组穷,戏服没几件,冯俊杰几乎靠一件背背佳撑起了整部剧。冯俊杰快速了解一下。四川人,来自觉醒东方,人...

唐山打人嫌犯陈继志去医院就医的背后,隐藏着三个精心设计的步骤

种种迹象表明,陈继志这帮人对处理打人之后的善后工作是轻车驾熟的,他们想实施的计划应该是这样的:首先第一步与伤者进同一家医院做伤情鉴定,鉴定级别最好要比对方严重,于是两位女伤者被鉴定为轻伤,他们就要求医...

取消回复欢迎 发表评论:

请填写验证码