进程内事件总线需用weakreference+concurrentdictionary实现可回收订阅,避免内存泄漏;asp.net core中eventbus注册为singleton,订阅者须绑定scoped生命周期并自动注销。

进程内事件总线(EventBus)在C#里不是框架自带的,而是靠开发者用 EventHandler、Action 或 IObservable 搭建的轻量通信机制;它不依赖外部中间件,但容易因生命周期管理不当导致内存泄漏或事件丢失——关键不在“怎么写”,而在“谁注册、谁注销、谁触发、谁接收”这四件事是否对得上。
为什么直接 new 一个 Dictionary> 不行
常见误区是手写一个全局字典存事件类型和委托列表,看似简单,实际踩坑密集:
-
Delegate引用未及时移除 → 订阅者对象无法被 GC,尤其在 ASP.NET Core 的 Scoped 服务中,可能让整个作用域“悬空” - 泛型事件类型擦除 →
EventHandler<usercreatedevent></usercreatedevent>和EventHandler<orderplacedevent></orderplacedevent>在运行时都是EventHandler`1,仅靠Type.GetGenericTypeDefinition()匹配会误判 - 同步发布时异常未捕获 → 一个订阅者抛异常,整个事件链中断,后续监听器收不到消息
- 没有线程安全控制 → 多线程并发
Subscribe/Unsubscribe可能引发InvalidOperationException(集合被修改)
用 WeakReference + ConcurrentDictionary 实现可回收订阅
核心思路:不让 EventBus 持有订阅者强引用,避免阻碍 GC。用 WeakReference<action>></action> 包裹回调,配合 ConcurrentDictionary<type list>></type> 存储。
实操要点:
- 订阅时用
new WeakReference<action>>(handler)</action>封装,不要存原始Action - 发布前必须调用
WeakReference.IsAlive和WeakReference.TryGetTarget(out handler)双重检查 - 每次
Publish都要遍历并清理已失效的WeakReference,否则字典持续膨胀 - 不要在
Subscribe里做反射解析参数类型——提前在注册时就确定好TEvent,避免每次发布都调用typeof(T).GetMethod("Invoke")
示例片段(简化):
private readonly ConcurrentDictionary<type list>> _handlers = new();
public void Subscribe<t>(Action<t> handler) where T : class
{
var eventType = typeof(T);
var weakRef = new WeakReference<action>>(handler);
_handlers.GetOrAdd(eventType, _ => new()).Add(weakRef);
}
public void Publish<t>(T @event) where T : class
{
if (_handlers.TryGetValue(typeof(T), out var refs))
{
var aliveHandlers = new List<action>>();
foreach (var wr in refs)
{
if (wr.IsAlive && wr.TryGetTarget(out var h)) aliveHandlers.Add(h);
}
// 清理失效引用(此处应原子替换 refs 列表)
foreach (var h in aliveHandlers) h(@event);
}
}</action></t></action></t></t></type>
ASP.NET Core 中如何让 EventBus 自动绑定生命周期
在 Web 应用里,EventBus 本身该注册为 Singleton,但订阅者常是 Scoped(如 IHostedService 或 Controller),这时必须确保:
- 订阅动作发生在 Scoped 容器创建后(例如在
ConfigureServices里只注册 EventBus,不立即Subscribe) - 取消订阅逻辑绑定到
IDisposable或IAsyncDisposable,并在 Scoped 结束时触发(比如在DisposeAsync()里调用Unsubscribe) - 避免在
Program.cs的顶级语句里直接eventBus.Subscribe(...)—— 此时 DI 容器未构建完成,this可能是静态类实例,造成跨 Scope 引用 - 若用 MediatR 等成熟库,它的
INotificationHandler<t></t>默认按 Scoped 解析,本质也是靠容器自动释放,自己实现时就得补上这套机制
真正难的不是发消息,是让“发”和“收”的生命周期在同一个上下文里对齐。很多线上内存泄漏,根源就是某个后台服务 Subscribe 了但忘了 Unsubscribe,而它的委托又闭包捕获了 DbContext 或 HttpClient —— 这些对象就跟着一起卡在内存里出不去。










