事件驱动框架EventNext之线程容器
EventNext
是.net core
下的一个事件驱动的应用框架,通过它代理创建的接口行为都是通过事件驱动的模式进行调用.由于EventNext
的所有调用都是基于事件队列来进行,所以在资源控制上非常方便;它可以进行多样性的线程分配,其中Actor
应用就是它的一种基础实现;在新的版中EventNext
增加了一个新的特性就是线程容器,通过线程容器可以让N个类的行为在指定线程资源下运行。接来详细分析这个功能的应用。
线程容器的便利性
平时在多线程应用的时候一般都用线程池或自己定义线程处理,但这些都需要手动去处理和调用。实际有些情况是希望不同实例的所有方法运行在指定线程资源下,但又不想入侵式修改代码,这样处理起来就比较复杂了。但EventNext
新版本的线程容器就能解决这种事情,它可以创建一个线程容器并指定那些对象的所有方法都在这个容器里执行,而容器的线程则可以根据实际情况来指定,这种完全透明的线程控制使用起来就非常方便(不过这版本的Actor暂不支持在线程容器中创建)。
接口定义准则
EventNext的所有任务都在由队列接管,但它实现的队列和平常的队列有所不同;EventNext的事件驱动队要求所有任务都用异步描述,它的特点是由上一个任务的完成来驱动下一下任务,不像同步处理队列在处理异步的时候不得不做线程等待。为了达到异步任务的要求接口的所有方法必须定义Task作为返回值(暂不支持属性处理)。
创建线程容器
组件提供一个EventNext.GetThreadContainer
方法来获取一个线程容器,方法有两个参数,一个是容器的名称和对应容器的线程数量;当容器被创建后内部的线程数量不变,具体代码如下:
var tc = EventCenter.GetThreadContainer("t1");
以上是获取一个名称为t1
的线程容器,在不指定线程数的情况下为一个线程处理。接下来就可以通过容器创建相应的接口实例:
var account = tc.Create<IAccount>();
以上是创建一个IAccount
的实例,这个实例不管在多少个线程下调用最终都由t1
这个线程容器去执行处理;每个容器可以创建多个接实例。
IAcouunt的实现
[Service(typeof(IAccount))] public class AccountImpl : IAccount { static AccountImpl() { Redis.Default.Host.AddWriteHost("192.168.2.19"); } public async Task Income(string name, int value) { await Redis.Default.Incrby(name, value); } public async Task<string> Value(string name) { return await Redis.Default.Get<string>(name); } }
以上是针对一个Redis
操作的实现,主要存在IO操作更容易测试到容器在不同线程下的差异。
测试代码
为了测试容器的效果,使用了不同的方式进行测试,分别是:不用线程容器和使用不同线程数的容器,测试代码如下:
-
非线程容器
static async Task None(int count) { var account = EventCenter.Create<IAccount>(); List<Task> tasks = new List<Task>(); var now = BeetleX.TimeWatch.GetElapsedMilliseconds(); for(int i=0;i<20;i++) { var t = Task.Run( async ()=> { for (int k = 0; k < count; k++) await account.Income("ken",k); }); tasks.Add(t); } await Task.WhenAll(tasks); var value = await account.Value("ken"); Console.WriteLine($"none use time:{BeetleX.TimeWatch.GetElapsedMilliseconds()-now} ms"); }
-
线程容器
static async Task Threads(int threads, int count) { var tc = EventCenter.GetThreadContainer($"t{threads}",threads); var account = tc.Create<IAccount>(); List<Task> tasks = new List<Task>(); var now = BeetleX.TimeWatch.GetElapsedMilliseconds(); for (int i = 0; i < 20; i++) { var t = Task.Run(async () => { for (int k = 0; k < count; k++) await account.Income("ken", k); }); tasks.Add(t); } await Task.WhenAll(tasks); var value = await account.Value("ken"); Console.WriteLine($"{threads} thread use time:{BeetleX.TimeWatch.GetElapsedMilliseconds() - now} ms"); }
运行代码
static async void Test() { Console.WriteLine("Warm-Up..."); await None(10); await Threads(1, 10); await Threads(2, 10); await Threads(4, 10); for (int i = 0; i < 4; i++) { Console.WriteLine("Test..."); await None(2000); await Threads(1, 2000); await Threads(2, 2000); await Threads(4, 2000); } }
测试结果
Test...
none use time:1231 ms
1 thread use time:3479 ms
2 thread use time:2025 ms
4 thread use time:1208 ms
Test...
none use time:1246 ms
1 thread use time:3358 ms
2 thread use time:2025 ms
4 thread use time:1179 ms
Test...
none use time:1344 ms
1 thread use time:3385 ms
2 thread use time:1954 ms
4 thread use time:1187 ms
Test...
none use time:1210 ms
1 thread use time:3338 ms
2 thread use time:1933 ms
4 thread use time:1181 ms
默认情况下组件会针对请调用进行一个线程分配,这个分配机制依据队列的负载情况进行分配调用;线程容器则会根据当前容器的线程数来进行一个平均分配处理,从测试结果可以看到不同线程数下完成所需要的时间。
示例代码
https://github.com/IKende/BeetleX-Samples