boost.asio包装类st_asio_wrapper开发教程(2015.11.6更新)(四)

如果你偶然浏览到这里,请先看  boost.asio包装类st_asio_wrapper开发教程(一)
源代码及例程下载地址:
git: https://github.com/youngwolf-project/st_asio_wrapper/,另外,我的资源里面也有下载,但不是最新的。
QQ交流群:198941541

十:陷阱
        大家都知道多线程死锁,进程间死锁,今天我要说的是,两台网络通信中的电脑,也会死锁,不可思议吧?那么st_asip_wrapper会死锁吗?请往下看。
        先举个网络编程中死锁的典型例子,假如AB两个程序做网络通信(谁是服务端谁是客户端无所谓),都采用单线程阻塞模式(虽然这是最低的层次,但总有很多人这样用,不管是出于什么原因,比如初学者、图简单、因数据流量小而能满足要求、赶时间等等),假设在某个时刻,A因发送缓存满而被阻塞在send,此时B(AB在不同电脑上,所以理论上,他们完全不相干涉,换句话说,无论A牌什么业务状态, B都有可能牌任意一个业务状态)完全有可能也因为发送缓存满而被阻塞在send,由于A被阻塞在send,说明B的接收缓存满了(不满的话,数据总是会从A到达B的,那么A的send就只会有限阻塞(死锁是无限阻塞)),由于B被阻塞在send,说明A的接收缓存也满了(原理同前面所说)。 两个套接字的四个缓存都满!此时,死锁就发生了,要想让A从send返回,B必须从自己的接收缓存里面读取一些数据,以便让A发送一些数据到B,腾空一些发送缓存。可是不幸的是,B被阻塞在send,它没机会执行recv来读取一些数据。反之已然。
        那么大家为什么很少遇到上面的情况呢,因为四个缓存都满,需要一些特定的条件,就是两边的发送一定要非常快,不一定要一直快,当死锁一但发生,马上把速度降下来也没用了(其实根本就没速度了,因为卡住了),死锁无法自解。如果你对你的产品做压力测试,相信可能会遇得上的。
        在说st_asio_wrapper是否会死锁之前,我们接着 教程一再说一下st_tcp_socket(st_udp_socket同理)的on_msg与on_msg_handle的区别,为什么要有他们俩,各有什么优势:
        当st_tcp_socket收到一条完整的消息的时候,它调用on_msg虚函数,那么显然,二次开发者可以重写on_msg,然后在里面处理消息(处理完之后一定要返回true,原因往下看),如果on_msg返回false,则消息进入st_tcp_socket的消息接收缓存,再通过调度机制(由boost的io_service.post()生成),异步的从消息接收缓存里面取出消息,然后调用on_msg_handle,二次开发者需要重写它并在里面处理消息。到此,大家看到了,消息处理有三种方法:
        1.重写on_msg,在里面处理消息,然后返回true;
        2.重写on_msg,什么也不做,直接返回false(重写是因为st_tcp_socket默认的on_msg返回true),再重写on_msg_handle,在里面处理消息;
        3.开启FORCE_TO_USE_MSG_RECV_BUFFER,在这种情况下得到的效果跟方法2完全一样,但效率更高,它不再调用on_msg(此时也没有这个虚函数了),而是直接把消息放入消息接收缓存。所以方法3在二次开发者开发的角度,跟方法2是一样的,以下说方法2的时候,统称方法2和方法3。
        方法一中,如果处理消息需要1秒,那么在这1秒之内,这个st_tcp_socket将无法接收任何数据,因为要等on_msg返回之后,才会接着接收数据;方法二中,因为是将消息直接放消息接收缓存,所以on_msg马上就返回了,那么马上就可以接着接收数据了(如果定义了FORCE_TO_USE_MSG_RECV_BUFFER也一样,根本不调用on_msg)。而on_msg_handle会在另外某个时刻的某个线程中被调用。很显然,方法二提高了响应时间(对方数据到达己方,己方什么时候开始读取数据,越早就越实时),但会消耗多一点资源(增加了一个消息接收缓存,多了一次on_msg_handle调度),这是第一个区别;
        第二个区别是,如果用不好它, 方法一会造成死锁,方法二则不会,纯异步多线程网络编程,还会死锁?——是的,原因是 增加了消息发送缓存,并且限制了缓存大小,请看我分析(这里面说的缓存,都是st_tcp_socket的缓存,不是套接字的缓存,异步IO下(包括linux的epoll,虽然它不是异步IO),套接字缓存满肯定不会造成阻塞):
        假设我们用上面的方法一,在on_msg中处理消息,假设在处理某一个消息的时候,一下就会产生出足够让消息发送缓存溢出的应答消息量(压力测试,平时很难遇上,所以大家可能会陌生),此时你在on_msg里面调用safe_send_msg,那么死锁可能就要发生了,因为对方可能也因为同样的原因也阻塞在on_msg(调用safe_send_msg),此时是不是就像单线程加阻塞模式了呢?A阻塞在on_msg等待自己的消息发送缓存可用,消息发送缓存要怎样才可用呢,就是A成功发送一部分数据到B,但此时B也阻塞在on_msg,无法返回,也就无法进行下一次的数据接收(前面讲了,on_msg返回之后,才进行下一次数据接收),那么A也就不可能成功发送数据到B了(这里指的是要成功接收了才算);如果你不阻塞在这里,那你怎么办呢,先找个地方把消息缓存住,然后在以后某个适当的时候再调用send_msg?这毫无疑问大大的提高了编程的难度,何况st_tcp_socket已经有消息发送缓存了,你还得自己再来一个,说不过去,而且你的这个消息发送缓存是不是也有大小限制的问题,那么是不是也有缓存溢出的问题……,唉,这样就没完没了了! 这里大家可能要问,缓存不加限制不就没问题了吗?答案是肯定不行,当发送方速度大于接收方处理速度时,缓存将无限增长直到内存耗尽!
        如果我们用方法二,是怎么避免发送消息缓存溢出的问题的呢?这涉及到两个问题( 消息接收缓存消息发送缓存):一,当on_msg返回false的时候,消息进入消息接收缓存,如果缓存溢出,则不进行下一次数据读取(前面说过,on_msg返回之后,马上接着进行下一次数据接收,是有点问题的,这里说的才是真实的情况),而是在未来的某个时刻再尝试把消息放入接收缓存,循环这个动作直到所有消息都成功放入消息接收缓存,然后再进行下一次数据接收,这样就避免了消息接收缓存的溢出问题;二,在on_msg_handle里面处理消息的时候,当然可能会像方法一一样,产生了大量的回应消息,也需要调用safe_send_msg,那么当消息发送缓存满的时候会怎样呢?由于阻塞在on_msg_handle不会阻塞数据的收发,所以safe_send_msg总会有返回的时候(因为对方的消息接收没有被阻塞,它总是会接收一些消息,于是会让己方的发送缓存变得可用)。
        注意, 只要不是因为等待缓存可能而阻塞在on_msg(两端都这样),都是没有问题的,大家也不要太害怕,比如你阻塞在on_msg里面处理自己的业务,或者阻塞在其它地方,比如on_msg_handle或者你自己的线程都是没有问题的。 换句话说,只要不阻塞在service线程里面,或者解除阻塞的条件与本st_socket的缓存可用性无关,就不会造成死锁,只会有限的阻塞,死锁是无限的阻塞。
        更方便简洁但有些暴力的方法是,用can_overflow为true调用send_msg,这个参数是2.1版本新添加的,目的就是不检测缓存溢出问题,这个就需要二次开发者保证缓存不会达到不可控的直线上升状态,也就是总要有让缓存减少的机制,或者业务本身在达到某个高度之后,就会下降,这些情况下,都可以让can_overflow为true。
        那么推荐的方法是,如果消息是凭空产生的,则调用safe_send_msg,如果消息是因为处理消息而产生的,则调用post_msg,具体请参看教程 第五篇。

        第三个区别关乎效率,对于方法一,如果你的处理速度足够快,那么其效率要高于方法二,因为消息不需要去消息接收缓存里面绕一圈;如果你的处理速度比较慢,那么方法二的效率要高于方法一,但高的不多,这种情况下,方法二的主要优点主要还是实时性,前面第一点说过了。方法二由于使用了接收消息缓存,其工作原理相当于一个电容,如果你的数据流量曲线是一条正弦曲线,且在最高流量时,处理速度慢于数据到达速度,在最低流量时,处理速度快于数据到达速度,在这种特定的情况下,方法二的效率就会得到更明显的体现,它会把正弦曲线状态的输入,转化为一条直线输出,达到了消息接收与消息处理都满负荷的运行(在这种情况下,方法一为什么效率明显低于方法二呢?因为在输入速度快于处理速度的时候,输入被阻塞;当处理速度快于输入速度时,处理被阻塞,就是两个人不齐心,没有忙到一块儿去)。注意,如果你的处理速度一直低于数据接收速度,那么任何方法都是免不了要被阻塞的,神仙也没办法。
        我前面说了这么多,什么死锁啊,阻塞啊,大家不要被吓住,其实绝大多数时候,都是不用考虑这些问题的,不管你用其它什么类库,或者自己写一个,都会有缓存满的问题,只是其它类库没有说明这些极限情况而已,我在这里提出来,只是想让大家知道有这么一回事。

十一:线程安全性
        默认情况下st_service_pump开启8个IO线程,只要线程数量多于1个,那么所有的回调方法(以on开关的虚函数),都是并发的( 除非是明显有顺序关系的,且是同一个st_socket对象的时候,比如同一个对象的每一条消息的on_msg总是在on_msg_handle之前被调用),具体说来(以on_msg和on_msg_send为例):对于同一个st_socket(及其子类对象),on_msg和on_msg_send是并发的,on_msg和on_msg、on_msg_send和on_msg_send是顺序的;对于不同的st_tcp_socket(及其子类对象),on_msg和on_msg_send、on_msg和on_msg、on_msg_send和on_msg_send都是并发的。
        关于on_timer的并发性,不同st_timer之间当然是并发,同一个st_timer的同一个timer(以id区分timer)的on_timer是顺序,不同的timer的on_timer是并发的。 注意:多线程对同一个st_timer调用set_timer设置同一个timer(id相同),是不安全的,设计即是这样。
        其它的方法,除了明显不应该设计为多线程的(比如初始化,开始结束服务等),都是线程安全的,比如send_msg等。

十二:编译时优化
        优化将用FORCE_TO_USE_MSG_RECV_BUFFER宏来实施,前面几篇教程都有提及,在这里,我将尽可能详尽的阐述其功能。
        前面说过了,on_msg()返回false,代表使用消息接收缓存,于是消息进入消息接收缓存,然后通过on_msg_handle分发。如果你决定在任何情况下都使用消息接收缓存,那么这里显然有个优化,可以不再调用on_msg()虚函数,而是直接将消息压入消息接收缓存,此时可以开启FORCE_TO_USE_MSG_RECV_BUFFER宏来做到这一点。注意,FORCE_TO_USE_MSG_RECV_BUFFER并不是必须,不定义FORCE_TO_USE_MSG_RECV_BUFFER也可以达到和定义FORCE_TO_USE_MSG_RECV_BUFFER宏完全一样的效果,只是效率差一点。一句话:定义FORCE_TO_USE_MSG_RECV_BUFFER(此时就没有on_msg这个虚函数了,不要再重写它,不会被调用)和不定义FORCE_TO_USE_MSG_RECV_BUFFER,但重写on_msg,并永远返回false,效果是完全一样的,只是前者效率高点。如果你决定某些时候使用消息接收缓存,某些时候不使用,即决策发生在运行时,则只能关闭FORCE_TO_USE_MSG_RECV_BUFFER,并把你的判断逻辑写在on_msg里面,以动态决定是否使用消息接收缓存(使用消息接收缓存意味着在on_msg_handle里面处理消息,它的好处与坏处前面说过了)。

十三:关于文件传输系统的使用
        这是我写的两个基于st_asio_wrapper的demo,file_server和file_client,支持简单的聊天和文件分块传输,在局域网里面,最好别分块,因为瓶颈在磁盘IO,如果从互联网上传输,则分块肯定可以提高速度,就像迅雷一样。
        使用方法很简单,把要传输的文件放到file_server能访问到的地方,开启file_server。file_client的运行方式是:file_client link_num,其中每一条连接代表一个文件块,如果要分5块同时下载,则link_num设为5,当file_client所有连接都成功连到服务器时,输入包命令:get file_name1 file_name2 ...,开始以次传输每一个文件。输入其它任何内容则当成聊天信息直接发送。如果传送的文件不在当前路径,比如 get ../123/a.txt,则要求客户端也必须存在../123这个目录,这个如果你觉得是问题的话,可以修改demo。

十四:关于测试客户端的使用
        测试客户端从3.0版本开始,已经从performance目录下面拿到了父目录,并且删除了原来的asio_client只留下test_client;而且在test_client里面,也不再使用st_test_client类(已经删除这了个类),而改为使用st_tcp_client,因为这个类已经在3.0版本里面支持多条连接了。关于测试客户端的部署及使用,在test_client目录下有个专门的文本文件来介绍。

st_asio_wrapper使用FAQ
boost.asio包装类st_asio_wrapper开发教程(五)

你可能感兴趣的:(boost.asio包装类st_asio_wrapper开发教程(2015.11.6更新)(四))