2009年3月29日星期日

线程上下文友好的RPC

最近工作需要,把系统重新构造为多个processes。以前的api调用有部分转换为RPC。因此需要做一个RPC的模块原型。RPC的基本实现一般操作系统上都有介绍,最简单的模型是(基于Message的同步RPC)

Client:                           
...                              
rpc_call(){                     
   send(api_id, args);          
   recv(ret);
   return ret; 
}
Server:
                          
while(...) {
   recv(api_id, args)
   ret = call(api_id, args);
   send(ret);
}

如果server只有一个线程,那么所有的RPC call都是同步的。一般不会这么简单的去使用,于是Server会创建线程来来处理,可能是为每个调用,也可能为每个连接。这也是目前看到一些流行RPC的做法,也是KISS的做法。可是,当你不是去做新东西的,而是为了改造以前的东西的时候,这些KISS的就不能方便的直接用了。 稍微复杂一点,双向的RPC调用的处理。普通函数调用经常也会有callback的情况。也就是说双方都会成为Server。如果对方发送callback的RPC调用到当前调用者,如何处理会比较好。简单的应用上面的模型,callback会在调用者进程某个处理RPC的线程内处理。如果在这个Callback内部又要调用到对方,也就是出现乒乓球一般的你来我往,那么每次跨越进程都会有新的线程产生。这的确不太好,而且还有一个问题,就是普通函数经常会有同步callback,而且这个callback的线程环境需要是和caller相同的环境。当有这样的假设时候,用户就可能在一些递归的锁上出问题,比如递归的Mutex.以前OK的code因为线程环境的更换而死锁。(不过,的确用户不应该做这样假设。) 新的简单的做法就是,调用者在发出调用请求后,并不是等待结果,而是进入和server类似的行为,就是和对方做回和,在调用者线程内处理对方的call。

Client:
...                              
rpc_call() { 
   send(api_id, args);

   while(...) {
      recv(msg);
      switch msg type:
          case ret: return ret;
          case call: call api from msg   
   }
   return ret;                       
}

在这里,server的RPC处理函数call内部可以rpc_call回到Client,产生互相的递归调用。同时借用的双方本地堆栈。(一开始我用SystemV的share memory和semaphore,就需要自己维护这个堆栈,稍微麻烦一点,里面的socket操作send和recv都需要用semaphore取代。不过感觉share memory的stack不安全,最后还是放弃了而是使用简单的socket,扩展更好,只是需要一些数据的拷贝开销)。这样,以前同步callback的代码专程RPC之后,线程上下文将保持一至,并且少了重新连接和新建线程的开销。 以上只是示意的伪代码,实际实现还需要不少要考虑的东西。不过我对这个找到这个简单的解法感到满意。程序员不是总是遇到可以随意发挥的时候,面对曾经的那些泥潭,憋屈的时候也能找到coding的乐趣。

0 COMMENTS: