Chinaunix首页 | 论坛 | 博客
  • 博客访问: 5762005
  • 博文数量: 675
  • 博客积分: 20301
  • 博客等级: 上将
  • 技术积分: 7671
  • 用 户 组: 普通用户
  • 注册时间: 2005-12-31 16:15
文章分类

全部博文(675)

文章存档

2012年(1)

2011年(20)

2010年(14)

2009年(63)

2008年(118)

2007年(141)

2006年(318)

分类: C/C++

2012-01-31 18:50:14

项目上有需要异步发送API,这两天对异步API的实现方法进行了一些思考。
同步发送API的设计相对简单,实现类似于send和recv的接口就可以了,recv阻塞等待应答的到来。但是这样会使得API的性能受限,除了采用多线程同步API通过并发来提高性能,另外一种方式就是通过异步发送API实现。
异步API的好处就是不用阻塞发送执行上下文,交由另外一个执行上下文进行具体的发送(可以是内核,也可以是另外一个线程),有应答的时候调用预先设置的callback。通常情况,由于执行callback的线程一般与调用API的线程是不同的。这个时候要注意线程安全,以及锁的处理。

同步发送API的消息交互流程图:
  1. client    --- message1 --->    server
  2. client    <--- ack1 ---            server
  3. client    --- message2 --->    server
  4. client    <--- ack2 ---            server
  5. client    --- message3 --->    server
  6. client    <--- ack3 ---            server
异步发送API的消息交互流程图:
  1. client    --- message1 --->    server
  2. client    --- message2 --->    server
  3. client    <--- ack1 ---            server
  4. client    <--- ack2 ---            server
  5. client    --- message3 --->    server
  6. client    <--- ack3 ---            server
异步发送API实现的时候,主要是两点:
1> message的发送没有前后依赖关系,后面的message发送的时候不用等待前面message的ack。
2> 收到应答的时候进行处理,调用设置的callback。

这里面第1点很容易实现,发送线程不停的发送就可以了,但是对于第2点如何判断收到应答,这个比较麻烦。因为传统的同步API是阻塞等待对方的应答,对于异步发送API,需要有一定的机制来获得应答。后面的几种异步发送API的实现方法中,分别采用不同的方式来获取应答。

1. 专门的发送线程,专门的接收线程
使用libpcap和libnet编写过程序的同学对这种模式不会陌生,业务线程或者是专门的发送线程使用libnet直接发送message,接收线程使用libpcap接收并进行处理。

这种方法的本质是,通过另外的接收线程同步接收应答。

2. event loop线程
业务线程构造发送数据,添加到event线程的req_list中。并通过Pipe通知event线程,event线程进行异步发送,有应答后,调用相应的callback处理。
event loop线程中实现消息的状态机读取,读取到完整的消息,进行相应的逻辑处理后,执行设置的callback函数。

这种方法的本质是,依靠select, poll, epoll等事件通知API实现,但是在linux下面只能拿到IO就绪事件,而不是IO完成事件,所以需要辅以协议状态机,转化为IO完成事件。

3. 后台发送线程
业务线程构造发送数据,添加到后台发送线程的req_list中,并通过pthread_cond来通知后台发送线程。后台线程直接进行发送,并检查是否有应答,如果有应答就进行处理。

这种方法的本质是,发送线程每发送1个message,就检测一些是否有应答到来。具体检测跟2的一样,也是通过select等实现。

伪代码如下:
  1. send:
  2.     req_queue.get_idx(snd_idx);
  3.     send();
  4.     select();
  5.     if(SUCCESS)
  6.         recv();
  7.         ack_idx=check_ack();
  8.         if(OK)
  9.             process();
  10.             req_queue.erase(ack_idx);
  11.         else
  12.             goto failed;
  13.     else if(ETIMEOUT)
  14.          snd_idx++;
  15.     else
  16.          goto failed;
  17.     
  18.     goto send;
  19. failed:
  20.     disconnect();

但是需要注意的一点是:方法2中select的等待时间是永远,但是这里面等待时间一般设置为0,使其立刻返回。有些实现中也会设置一个较小的值,例如10ms,也就是说每次发送都会select等待10ms,如果10ms内有应答,就处理应答;如果10ms内没有应答,就继续发送。
设想一下,如果每次应答时间为15ms,那么采用这种异步发送方式,启动后前10ms,发送完一个message后,就会阻塞在select上,导致select超时,然后发送下一个message。发送第2个message后的select会在5ms后被唤醒,因为第1个message的ack到达了,这样启动后满负荷跑的话,每次send后,select都会很快返回(小于10ms,甚至是立刻返回),接受到前面message的ack。这样发送的性能就会有所提升。

对于上面3种实现方式,推荐使用第2种。当多个线程调用异步发送API的时候,只需要1个event loop线程就好了。如果第3种,select的timeout为0的话,空select系统调用执行的次数也会比较多。

参考:

阅读(10755) | 评论(0) | 转发(0) |
1

上一篇:使用lua给wireshark编写uTP的Dissector

下一篇:没有了

给主人留下些什么吧!~~