IOCP完全开发经验总结(二):几个重要问题分析(上)

7 篇文章 1 订阅
6 篇文章 0 订阅

我在小猪的博客里回复了很多比较重要的问题,这里会花费大量的精力去研究解决这些问题。

WSASend

小猪的文章里并没有说WSASend如何安全的去用,只是一句话带过,说这个很简单,带着要严谨的科学和研究精神,我看了很多源码示例(包括说是有个很牛逼的老外写的),都没有详细的说这一部分,后来我又找了很多关于WSASend理论(包括MSDN),才总结了一些东西:

1、WSASend如果作为同步IO发送(与Send作用相同时),是非线程安全的,不能同时在多个线程中同时调用。

这个完全理解。

2、WSASend作为异步IO发送时,虽然是非线程安全的,但你可以放心的在多个线程中调用(-_-!)。

微软说的这个让人匪夷所思,我个人认为它不太可能是用了原子操作,而是windows系统本身就是个抢占式OS,也就是说,除非一个线程本身放弃CPU执行权,否则它会一直占用到死。所以它是不会把数据复制到协议栈一部分时跑去执行另一个线程代码。但我认为,对于多核CPU(此时是并行而不是并发),它还是有出错的几率(虽然非常非常低)。

3、不需要对发送失败的WSASend进行重发

这个忘了从哪里看到的了,不过确实如此,如果发送失败,说明TCP连接已经断开了,因为TCP协议本身就是保证传输的可靠性的。

4、其他问题

有博文称,虽然多线程调用WSASend是可以的,但是当你正分段发送一个大数据时,如果正好碰到了发送心跳的线程发送了一个字节的心跳,那么这一颗老鼠屎就坏了一锅汤,整个大数据就全部不能用了。所以,我甚至把心跳的WSASend也放在了同一个线程里。

5、我的方案

虽然上面说了这么多,多心的我还是只在一个线程中调用了WSASend,而且对发送失败的WSASend进行重新发送。

多个还是单个WSARecv、WSASend

我的方案是,一个SocketContext使用一个WSARecv和一个WSASend。下面举出反例:
如果用多个WSARecv,假设数据是源源不断且量非常巨大的发到Server端,虽然完成端口队列是FIFO的,且取出的数据也是按顺序的,但我经过实际测试(使用PostQueuedCompletionStatus),还是会导致包的顺序混乱。原因也很简单,虽然Windows是抢占式OS,但多核CPU的线程有可能会并行的(not并发),所以会导致多个拿到队列中WSARecv数据的线程处理顺序不确定,比如后拿到WSARecv数据的线程会先处理,这就产生了顺序混乱问题。况且也不能保证咱的代码逻辑上完全没问题,所以还是安心用一个WSARecv吧。
至于多个WSASend,如果真的和微软说的,WSASend正常情况下不会发送失败的话,我认为多个也无妨,但假设会失败,那你发送了N个WSASend,其中如果一个出了问题,你再重发,那仍然导致了包乱序的问题。
反正就一个宗旨:服务端是不能出现任何问题的。那就多写几句放心的代码吧。

多线程VS单线程

当初没想到这个问题,直到看到一个博客,觉得说的也很有道理:

在绝大多数讲解IOCP的文章中都会建议使用多个工作线程来处理IO事件,并且把工作线程数设置为CPU核心数的2倍。根据我的印象,这种说法的出处来自于微软早期的官方文档。不过,在我看来这完全是一种误导。IOCP的设计初衷就是用尽可能少的线程来处理IO事件,因此使用单线程处理本身是没有问题的,这可以使实现简化很多。反之,用多线程来处理的话,必须处处小心线程安全的问题,同时也会涉及到加锁的问题,而不恰当的加锁反而会使性能急剧下降,甚至不如单线程程序。有些同学可能会认为使用多线程可以发挥多核CPU的优势,但是目前CPU的速度足够用来处理IO事件,一般现代CPU的单个核心要处理一块千兆网卡的IO事件是绰绰有余的,最多的可以同时处理2块网卡的IO事件,瓶颈往往在网卡上。如果是想通过多块网卡提升IO吞吐量的话,我的建议是使用多进程来横向扩展,多进程不但可以在单台物理服务器上进行扩展,并且还可以扩展到多台物理服务器上,其伸缩性要比多线程更强。
当时微软提出的这个建议我想主要是考虑到在IO线程中除了IO处理之外还有业务逻辑需要处理,使用多线程可以解决业务逻辑阻塞的问题。但是将业务逻辑放在IO线程里处理本身不是一种好的设计模式,这没有很好的做到IO和业务解耦,同时也限制了服务器的伸缩性。良好的设计应该将IO和业务解耦,使用多进程或者多线程将业务逻辑放在另外的进程或者线程里进行处理,而IO线程只需要负责最简单的IO处理,并将收到的消息转发到业务逻辑的进程或者线程里处理就可以了。

原文链接:IOCP编程小结
确实,使用单线程的话真会解放很多不必要的麻烦。但我的项目里仍然用了多线程,具体看下面的锁的问题。

锁的问题:用还是不用

废话,当然能不用就不用了!
起初我项目里至少用了3个锁,分别用来锁两个池(SocketContext和IOContext池)和一个Socket队列。因为有些逻辑在工作线程中处理了,比如AcceptIO返回时,得向Socket队列中加入一个新Socket,从两个池中申请一个IOContext和一个SocketContext继续处理,所以这个函数甚至同时会用到3个锁,另外还有心跳线程遍历时也会锁住Socket队列,这些都会导致运行效率的低下。
后来我突然醒悟,小猪的代码可能有点误导,他在工作线程中处理了这些东西,那为何不统一到一个线程中处理呢?换个思维,工作线程只是用来传递数据,然后我们新开辟一个线程(也可以是主线程)来操作两个池、操作和遍历Socket队列、统一调用WSASend不就都解决了?所以我花了一周时间来重写了这块,工作线程所有的操作全都使用事件来发送给主线程来处理(当然你也可以重新开个线程),所以可以去掉所有的锁了。目前运行很顺利,效率就更不用说了。
另外因为我用的是Qt,很方便的使用信号槽来给不同线程发送信号(事件),经测试信号槽发送不会因为队列数据过多而发送失败(只会一直涨内存),而以前测试的MFC使用PostMessage时,队列过多会返回FALSE,我也懒得研究过时的东西了,大家只需注意保证成功给处理线程发送事件即可。

参考文章:

IOCP编程小结
多线程下的神奇的IOCP

评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值