C#中实现并发的几种方法的性能测试

C#中实现并发的几种方法的性能测试

0x00 原由

去年写的一个程序由于须要在局域网发送消息支持一些命令和简单数据的传输,因此写了一个C/S的通讯模块。当时的作法很简单,服务端等待连接,有用户接入后开启一个线程,在线程中运行一个while循环接收数据,接收到数据就处理。用户退出(收到QUIT命令)后线程结束。程序一直运行正常(固然还要处理“TCP粘包”、消息格式封装等问题,在此不做讨论),不过随着使用的人愈来愈多,并且考虑到线程开销比较大,若是有100个用户连接那么服务端就要多建立100个线程,500个用户就是500个线程,确实太夸张了(固然实际并无那么多用户)。因为TCP通讯并非每时每刻都在进行着的,所以能够把全部客户端链接存储到一个列表中,经过轮询的方式依次开启一个线程进行数据接收,接收完毕后释放线程,这样能够充分利用线程池,避免大量线程消耗内存和CPU。html

轮询的方式经过线程池实现了线程的复用,能够确定的是在资源开销上确定是小不少的,但轮询的方式在单位时间内的处理次数会不会比保持线程的方式少不少呢,本测试将解决这个疑问。git

0x01 实验方法

IDE:VS2015github

.Net Framework 4.5多线程

接收数据的对象以下所示并发

 

经过ReceiveData方法接收数据,每次接收只有1%的可能性收到数据,经过建立N个对象接收数据来模拟一个TCP服务端处理N个链接的状况。毕竟TCP通讯不是随时进行的,固然这个百分比能够调整。程序输出的内容包括每秒执行了多少次接收操做,接收到数据的线程编号和接收到的内容等。异步

0x02 保持线程的并发

保持线程的并发很是直观,就是每创建一个对象就开一个新线程循环进行ReceiveData操做,当接收到数据就把相关信息输出到主界面上。代码以下所示:async

 

0x03 使用ThreadPool轮询并发

方法是使用一个List(或其余容器)把全部的对象放进去,建立一个线程(为了防止UI假死,因为这个线程建立后会一直执行切运算密集,因此使用TheadPool和Thread差异不大),在这个线程中使用foreach(或for)循环依次对每一个对象执行ReceiveData方法,每次执行的时候建立一个线程池线程来执行。代码以下:高并发

 

0x04使用Task轮询并发

方法与ThreadPool相似,只是每次建立线程池线程执行ReceiveData方法时是经过Task建立的线程。代码以下所示:性能

 

0x05 使用await轮询并发

方法与ThreadPool相似,只是每次建立线程池线程执行ReceiveData方法时是经过await等待操做。代码以下:测试

刚开始在foreach中写了await致使线程阻塞,但由于ReceiveData()中测试时为了尽可能拉开差距没有让线程睡眠以模拟线程操做,致使没有意识到这个问题,多谢 @逸风之狐 提醒。

修改后代码以下所示,这样测试方法就能够当即返回了。不过async/await确实不是用来干这个的。

 

0x06 使用Parallel并发

这是FCL提供的一种方法,Parallel.ForEach中每次方法都是异步执行,执行采用的是线程池线程。代码以下所示:

 

0x07 测试结果

建立500个对象来模拟500个链接的状况。其中测试结果中的每秒接收次数会有个波动范围,主要参照百位以上。使用线程池线程的几个方法(ThreadPool、Task、await、Parallel)中程序的线程数略有差异,可能跟执行环境有关,难以代表实质性差别。其中await由于线程切换致使线程执行时间略长,使得线程池须要多建立一些线程。

1、保持线程的并发

 

平均每秒接收8654次数据。在任务开始后会建立500个线程,因为每一个线程都须要单独的栈空间来执行,内存消耗较大。频繁切换线程也会加剧CPU的负担。

2、ThreadPool轮询并发

 

平均每秒接受9529次数据。因为实现了线程池线程的复用,无需建立太多线程,内存没有出现波动,CPU消耗也比较均匀。

3、Task轮询并发

 

平均每秒接收9322次数据,因为Task也是基于线程池的封装,所以与ThreadPool结果差异不大。

4、await轮询并发

 

平均每秒接收4150次。await也是使用线程池线程,因此在内存开销和线程数上与其余使用线程池线程的方法没有太大差异。但await在等待完毕后会将执行上下文从线程池线程切换回调用线程,所以CPU开销较大。

5、Parallel并发

 

看名字就知道这个设计出来就是应用于这种使用环境的,平均每秒接收9387次数据,也是使用线程池线程,因此内存和CPU消耗与ThreadPool和Task差很少。但不须要本身写foreach(for)循环,只要写循环体便可。

六、补充测试

经测试随着ReceiveData()耗时不断增长,轮询方式的优点愈来愈小。表现就是刚开始线程执行效率很低,须要花费时间慢慢遇上去。由于线程池中的初始线程不够用,须要建立更多的线程池线程,线程池线程建立起来没有Thread那么快,不过当线程池中的线程数量逐渐知足需求以后,轮询的优点就又体现出来了。

测试1:测试一样500个线程,有1%的可能接收到数据,但收到数据时模拟执行操做耗时100毫秒,程序刚开始效率很低,花了大概12秒左右,当线程数增加到54个时基本稳定能够知足需求,效率也愈来愈高。

测试2:测试一样500个线程,有1%的可能接收到数据,但收到数据时模拟执行操做耗时500毫秒,程序刚开始效率一样很低,花了大概150秒左右,当线程数增加到97个时基本稳定能够知足需求,效率也愈来愈高。

0x08 结论

首先明显能看出来的是使用轮询的方式比保持线程能节省不少资源,特别是内存。并且在处理效率上轮询的方式(每秒接收9300-9500次)比保持线程还要高(每秒8600+)。所以在这种并发模型下应该使用轮询的方式以节省资源并提升并发效率。

实际上硬拿await来比较是不太公平的,await被设计出来就不是应用于这种场景的。不论是以前关于异步的测试仍是并发的测试,基于线程池的方案相差都不大。所以思路对了的状况下使用ThreadPool老是没错的。但有些类型把ThreadPool包装了以更好适应某些特殊场景,所以有了Task、await、Parallel等。而在此次的测试条件下显然Parallel是最合适的,与直接使用ThreadPool相比资源开销和执行效率同样,但代码更少。

在补充测试中也能看到,不一样的运行环境对运行效率的影响仍是很大的,所以仍是要针对本身的环境作针对性更强的测试以采用更合适的方法。例如在个人使用环境中,服务端TCP消息的转发和部分命令的处理耗时都是很是短的。一样假设最高同时在线500个用户,这500个用户也不会是同事登录的,因此也不会存在线程池初始线程严重不够用的状况。随着用户慢慢登录,线程池线程根据需求慢慢增长,这样建立线程池线程增长的耗时就不那么明显了。因此在个人使用环境下轮询的方式无疑是合适的。所以刚开始对ReceiveData()只设置了接受数据的几率,没有模拟延迟。你们有需求的能够把测试程序下下来根据实际状况调整最大并发数、接收到数据的几率和接收数据的耗时以进行测试。

0x09 相关下载

测试代码下载连接:https://github.com/durow/TestArea/tree/master/AsyncTest/ConcurrenceTest

 


更多内容欢迎访问个人博客:http://www.durow.vip

相关文章
相关标签/搜索