异步通讯中使用纤程(Fiber/UserSpaceThread)

在异步通讯中,一般使用一个线程来select/poll/epoll,收到信号后,解码消息头,或者整个消息,然后将相应的fd交给其他线程处理。这看上去的确是个很好的办法,但是……

举个例子,我们在写一个网络程序,遇到一个复杂的派发过程:收到的消息大小未知,而只能使用序列化方法解码消息然后根据其内容进行派发。这种情况的特征是:在大多数情况下,都可以无阻塞地读取并解码整条消息,但也有极小的可能在某条消息上阻塞(消息不完整)。

这种情况往往使用线程池,select线程一得到可读信号,就把相应的fd交给线程池处理。这样是解决了问题,但是又有个新问题:线程切换的开销太大。如果在高速局域网内,并且每个请求的实际执行时间非常短,问题就会比较显著。

想了很久,也想出了一个解决办法:利用Fiber/UserSpaceThread,中文译作纤程/用户空间线程,不懂的看这里

select线程收到读信号后,仍然执行解码,如果消息不完整,就将当前执行绪切换到Select执行绪,用行话说就是:将当前Fiber挂起,切换到Select Fiber。因为这多个Fiber都在用户空间中运行,所以速度非常快,并且,还不需要内核调度。其实速度的提升主要不是在这里,而是在大多数情况下不需要切换线程。

 

作者:
该日志由 rockeet 于2008年11月15日发表在并发, 操作系统分类下, 你可以发表评论,并在保留原文地址及作者的情况下引用到你的网站或博客。
转载请注明: 异步通讯中使用纤程(Fiber/UserSpaceThread)
标签:
【上一篇】
【下一篇】

您可能感兴趣的文章:

8 个回复

  1. ken16说道:

    可以从 reactor 和 proactor 的角度来说这个事情。

    >>这种情况往往使用线程池,select线程一得到可读信号,就把相应的fd交给线程池处理。

    这个是 reactor 的做法。

    >>select线程收到读信号后,仍然执行解码,如果消息不完整,就将当前执行绪切换到Select执行绪

    这个是 proactor 的做法。

    在 java 中,可以看看 mina 框架的具体实现。

    http://blog.163.com/rhythm-zju/blog/static/31004200801504351529/

  2. whinah说道:

    你可能没明白我的意思,reactor/proactor, 跟我这里说的不是一回事,在我做的那个rpc中,收到(在buffer中,未解码)数据时,因为无法知道这个数据到底有多长,只有在解码完成时,才知道这个数据的确是结束了。如果要使用reactor/proactor,就需要知道解码的当前状态,而这个状态是在堆栈中的(也可以不用堆栈,而使用显式的状态机,使用状态机就无法做通用解码器)。因此,在这种情况下,最重要的保存解码的状态,而不是reactor/proactor。
    使用多线程,解码的状态保存在线程堆栈中,但是线程切换的开销太大,使用Fiber/Coroutine,在同一个线程中,可以拥有多个堆栈(保存多个解码状态/状态机),而Fiber切换的开销要小得多!

  3. ken16说道:

    >>就需要知道解码的当前状态,而这个状态是在堆栈中的(也可以不用堆栈,而使用显式的状态机,使用状态机就无法做通用解码器)

    mina 就是用显式的状态机,叫做 MessageDecoder ,里面有一个 decode 的虚方法,由具体的应用来实现。这里不需要通用解码器,而是框架要提供一个接口,可以用应用来提供解码器。

    Interface MessageDecoder {

    MessageDecoderResult decode(IoSession session,
    IoBuffer in,
    ProtocolDecoderOutput out)
    throws Exception

    Decodes binary or protocol-specific content into higher-level message objects. MINA invokes decode(IoSession, IoBuffer, ProtocolDecoderOutput) method with read data, and then the decoder implementation puts decoded messages into ProtocolDecoderOutput.

    Returns:
    OK if you finished decoding messages successfully. NEED_DATA if you need more data to finish decoding current message. NOT_OK if you cannot decode current message due to protocol specification violation.
    Throws:
    Exception – if the read data violated protocol specification

    }

  4. ken16说道:

    通用的解码器,从泛的角度来说,不可能存在,http 协议的解码器和 ldap 的解码器肯定是不同的。但是在一个小范围内,如果自己要设计一系列的交互协议,倒是可以做成统一的格式,比如最简单的 cmd + len + /r/n + data 的格式。
    DATA <length> /r/n
    <…..>

    这样在小范围内可以有一个通用的解码器。这个解码器,其实不是解码器,只是一个判断协议包是否完整的 callback 。就上面提到的问题来说,也就是需要这样一个 callback 。

  5. whinah说道:

    在我这里(RPC),通用的解码器有如下的模式:
    input >> arg1 >> arg2 >> arg3 …;
    至于 argX 的 decoder 如何实现,框架并不知道,框架只知道 argX 需要读入一些数据,做一些转换。因为在读入的任何时刻,都可能发生缓冲被读空,在这个时候(被读空),解码的状态都在堆栈中(比如某个结构的某个int32成员刚读了两个字节)。因此,也只能使用堆栈保留这个状态,这时用Coroutine效率是最高的(如果只使用线程,一个线程就只能保存一个连接的解码状态)。
    你说的那个cmd + len + /r/n + data 例子,实际上已经限定了消息的格式,它可以用一个固定的状态机来表达。而我这里就无法使用这样固定的状态机,只能使用堆栈。

  6. whinah说道:

    在mina中的MessageDecoder.decode中,如果解码解到一半(在用户代码的某个深层函数调用中),发现need_data,怎么办?要处理这种情况,用户代码就必须保存一个状态,这个状态需要保存在哪里?

  7. ken16说道:

    >>你说的那个cmd + len + /r/n + data 例子,实际上已经限定了消息的格式,它可以用一个固定的状态机来表达。而我这里就无法使用这样固定的状态机,只能使用堆栈。

    只有知道了消息格式才能写解释器,不知道消息格式怎么能够解释呢?

    >>在mina中的MessageDecoder.decode中,如果解码解到一半(在用户代码的某个深层函数调用中),发现need_data,怎么办?要处理这种情况,用户代码就必须保存一个状态,这个状态需要保存在哪里?

    http://svn.apache.org/viewvc/mina/branches/1.0/example/src/main/java/org/apache/mina/example/httpserver/codec/HttpRequestDecoder.java?revision=555855&view=markup

    这是一个 http 的 request decoder 。状态就保存在 decoder 自身的成员变量中。

    public MessageDecoderResult decode(IoSession session, ByteBuffer in,
    ProtocolDecoderOutput out) throws Exception {
    // Try to decode body
    HttpRequestMessage m = decodeBody(in);

    // Return NEED_DATA if the body is not fully read.
    if (m == null)
    return MessageDecoderResult.NEED_DATA;

    out.write(m);

    return MessageDecoderResult.OK;
    }

  8. whinah说道:

    >>只有知道了消息格式才能写解释器,不知道消息格式怎么能够解释呢?
    当然,解码的前提就是知道它的数据格式,我们可以coding一个显式的状态机(一大堆swich-case),来保证在每次缓冲被读空时,状态机处于一个可控的状态。当数据再次到来,从那个状态继续开始。这样的好处是同一个入口函数(如Decoder.decode)可以安全地被多次调用来解码一条(部分完成)的消息。可以使用reactor/proactor来有效地进行demultiplex,这时只使用一个线程,没有线程切换的开销;但是编码显然复杂得多。
    也可以使用通常的顺序代码,不显示地保存状态,读到哪算哪,没数据就等待,传统上就是阻塞式调用。这样编码简单(堆栈状态相当于Decoder.decode中保存的那个显式状态),但是一个线程只能处理一个连接,这是因为一个线程只有一个堆栈,而这种模式不能通过多次调用类似(Decoder.decode)来解码(部分完成的)消息。
    使用Fiber/Coroutine可以在一个线程中使用多个堆栈,从而突破了这个限制,但也并非没有缺点,Fiber的堆栈在地址空间中一般至少要两个系统页(一个Commited Page,一个guard page,共8K,但是Windows下至少要64K,因为一个AllocationGranularity是64K),但实际使用的栈空间往往很小(很少,甚至永远不会大于1K),并且切换的代价仍然比函数调用大得多(但比Thread切换要小得多,这恐怕也是Fiber比起Thread唯一的优势)。

发表评论

您必须 登录 后才能发表评论。