C# 订阅发布方式技术总结(观察者模式)
一 技术目标
客户端程序开发的过程中,订阅发布技术是使用频次比较多的,如:界面通知,操作通知等,订阅发布可以分为应用内的和应用间的,本次主要是总结应用内的观察者模式实现,应用间的可通过第三方工具(MQ,Redis,RPC等)来实现。
订阅发布,关注的是效率,大量通知产生的时候,可以及时处理完成,所以从完成时间这个标准来衡量我使用到的集中方式。
二 技术过程
- 委托
委托、事件是.net提供的实现方式,简单实现如下:
// 1. 委托
public delegate void TestDelegate(string msg);
// 2. 事件
public event TestDelegate TestEvent;
// 3. 订阅事件(示意)
public void SubjectEvent()
{
TestEvent += (msg) =>
{
Console.WriteLine($"事件接受消息:{msg}");
};
}
// 4. 发布
public void PublishEvent(string msg)
{
TestEvent?.Invoke(msg);
}
- 事件总线
基于事件总线的通知,可以以类的方式,进行数据传递,使用字典记录当前Handle程序和发布程序即可,具体程序如下:
using Error.Extension;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Util.EventBus.Obj
{
public class EventBus
{
#region 单例
private static EventBus eventBus = new EventBus();
public static EventBus Instance // 单例
{
get { return eventBus; }
}
#region 构造函数
private EventBus()
{
}
#endregion 构造函数
#endregion 单例
#region 变量
private readonly object locObj = new object(); // 订阅/发布线程锁
private static Dictionary<Type, List<object>> dicEvents = new Dictionary<Type, List<object>>(); // 事件数据的存储,目前采用内存字典
#endregion 变量
#region 方法
#region 判断事件处理程序是否相同
private readonly Func<object, object, bool> eventHandlerEquals = (o1, o2) =>
{
var o1Type = o1.GetType();
var o2Type = o2.GetType();
if (o1Type.IsGenericType &&
o1Type.GetGenericTypeDefinition() == typeof(ActionDelegatedEventHandler<>) &&
o2Type.IsGenericType &&
o2Type.GetGenericTypeDefinition() == typeof(ActionDelegatedEventHandler<>))
return o1.Equals(o2);
return o1Type == o2Type;
};
#endregion 判断事件处理程序是否相同
#region 订阅
#region 订阅事件列表
public void Subscribe<TEvent>(IEventHandler<TEvent> eventHandler)
where TEvent : class
{
lock (locObj)
{
var eventType = typeof(TEvent);
if (dicEvents.ContainsKey(eventType))
{
var handlers = dicEvents[eventType];
if (handlers != null)
{
if (!handlers.Exists(deh => eventHandlerEquals(deh, eventHandler)))
handlers.Add(eventHandler);
}
else
{
handlers = new List<object>();
handlers.Add(eventHandler);
}
}
else
dicEvents.Add(eventType, new List<object> { eventHandler });
}
}
#endregion 订阅事件列表
#region 订阅事件实体
public void Subscribe<TEvent>(Action<TEvent> eventHandlerFunc)
where TEvent : class
{
Subscribe(new ActionDelegatedEventHandler<TEvent>(eventHandlerFunc));
}
#endregion 订阅事件实体
#region 订阅多个事件实体
public void Subscribe<TEvent>(IEnumerable<IEventHandler<TEvent>> eventHandlers)
where TEvent : class
{
foreach (var eventHandler in eventHandlers)
Subscribe<TEvent>(eventHandler);
}
#endregion 订阅多个事件实体
#endregion 订阅
#region 取消订阅事件
public void Unsubscribe<TEvent>(Action<TEvent> eventHandlerFunc)
where TEvent : class
{
Unsubscribe<TEvent>(new ActionDelegatedEventHandler<TEvent>(eventHandlerFunc));
}
#region 取消订阅
public void Unsubscribe<TEvent>(IEventHandler<TEvent> eventHandler)
where TEvent : class
{
lock (locObj)
{
var eventType = typeof(TEvent);
if (dicEvents.ContainsKey(eventType))
{
var handlers = dicEvents[eventType];
if (handlers != null
&& handlers.Exists(deh => eventHandlerEquals(deh, eventHandler)))
{
var handlerToRemove = handlers.First(deh => eventHandlerEquals(deh, eventHandler));
handlers.Remove(handlerToRemove);
}
}
}
}
#endregion 取消订阅
#region 取消多个订阅事件
public void Unsubscribe<TEvent>(IEnumerable<IEventHandler<TEvent>> eventHandlers)
where TEvent : class
{
foreach (var eventHandler in eventHandlers)
Unsubscribe<TEvent>(eventHandler);
}
#endregion 取消多个订阅事件
#endregion 取消订阅事件
#region 发布
#region 异步发布
public void Publish<TEvent>(TEvent evnt)
where TEvent : class
{
if (evnt == null)
throw new ArgumentNullException("事件源不能为空");
var eventType = evnt.GetType();
if (dicEvents.ContainsKey(eventType)
&& dicEvents[eventType] != null
&& dicEvents[eventType].Count > 0)
{
var handlers = dicEvents[eventType];
foreach (var handler in handlers)
{
var eventHandler = handler as IEventHandler<TEvent>;
eventHandler.Handler(evnt);
}
}
}
#endregion 异步发布
#region 有回调的发布
public void Publish<TEvent>(TEvent evnt, Action<TEvent, bool, Exception> callback, TimeSpan? timeout = null)
where TEvent : class
{
if (evnt == null)
throw new ArgumentNullException("事件源不能为空!");
var eventType = evnt.GetType();
if (dicEvents.ContainsKey(eventType) &&
dicEvents[eventType] != null &&
dicEvents[eventType].Count > 0)
{
var handlers = dicEvents[eventType];
List<Task> tasks = new List<Task>();
try
{
foreach (var handler in handlers)
{
var eventHandler = handler as IEventHandler<TEvent>;
tasks.Add(Task.Factory.StartNew((o) => eventHandler.Handler((TEvent)o), evnt));
}
if (tasks.Count > 0)
{
if (timeout == null)
Task.WaitAll(tasks.ToArray());
else
Task.WaitAll(tasks.ToArray(), timeout.Value);
}
callback(evnt, true, null);
}
catch (Exception ex) when (ex.Log()) { }
catch (Exception ex)
{
callback(evnt, false, ex);
}
}
else
callback(evnt, false, null);
}
#endregion 有回调的发布
#endregion 发布
#endregion 方法
}
}
using System;
namespace Util.EventBus.Obj
{
public class ActionDelegatedEventHandler<TEvent> : IEventHandler<TEvent>
{
#region 构造函数
public ActionDelegatedEventHandler(Action<TEvent> eventHandle)
{
action = eventHandle;
}
#endregion 构造函数
#region 变量
private Action<TEvent> action; // 处理程序
#endregion 变量
#region 方法
public void Handler(TEvent t)
{
action?.Invoke(t);
}
#endregion 方法
}
}
namespace Util.EventBus.Obj
{
/// </summary>
public interface IEventHandler<TEvent>
{
void Handler(TEvent t);
}
}
// 1. 订阅
public void SubjectObj()
{
EventBus.Instance.Subscribe<NoticeEntity>(m =>
{
Console.WriteLine($"接收到消息:m");
});
}
// 2. 发布
public void PublishOjb()
{
EventBus.Instance.Publish(new NoticeEntity());
}
- 第三方组件PubSub
第三方组件PubSub,支持观察者模式,使用Nuget导入:

使用如下: // 发布
public void SubscribePubHub(Action<string> action)
{
Hub.Default.Subscribe(action);
}
// 订阅
public void PublishPubHub(string t)
{
if (t == null) return;
Hub.Default.PublishAsync(t);
}
- 第三方组件RX
第三方组件RX,支持观察者模式,RX基于线程池优化,速率最快,使用Nuget导入:

使用如下:
#region 发布
public void Publish(T t)
{
try
{
if (!Sub.IsDisposed)
Sub.OnNext(t);
}
catch (Exception ex) when (ex.Log()) { }
catch (Exception ex)
{
throw ex;
}
}
#endregion 发布
#region 订阅
public void Subject(Action<T> action, Action<Exception> errorAction = null, Action completeAction = null)
{
#region 校验
if (action == null) throw new ArgumentException($"订阅响应回调不能为空!");
#endregion 校验
try
{
if (!Sub.IsDisposed)
{
Action<T> asyncAction = t => { Task.Factory.StartNew(() => action?.Invoke(t)); };
Sub.SubscribeOn(TaskPoolScheduler.Default).Subscribe(action, (ex) => { errorAction?.Invoke(ex); }, () => { completeAction?.Invoke(); });
}
}
catch (Exception ex) when (ex.Log()) { }
catch
{
throw;
}
}
#endregion 订阅
三 技术模式
上述四种方式测试测试1000000条字符串数据的通知完成时间,效果如下:

所以目前,选择使用RX封装的程序内通知机制。
四 存在问题
五 总结

浙公网安备 33010602011771号