Programming 版 (精华区)

发信人: zhangyan (塑料脑袋镀浆糊), 信区: Programming
标  题: 科学的函数调用包装 
发信站: 哈工大紫丁香 (2001年04月20日18:04:50 星期五), 站内信件


科学的函数调用包装 作者:张岩 发布时间:2001/04/19
文章摘要:
  当今的软件是由越来越多的并发的线程执行而构成的。跨线程通讯通常是由异步调
用来实现的。将一个函数调用封装并执行的过程是非常直接和简单的。然而,在一个异
步调用多个函数的程序中,这可以引起代码膨胀。本文给出一种减少代码膨胀并使异步
调用更容易实施的方法。这个方法只是在运行时引起一些间接调用的额外开销。
  关键词 线程,函数,模板,函数模板
----------------------------------------------------------------------------
----
正文:
科学的函数调用包装 
简介
  当今的软件是由越来越多的并发的线程执行而构成的。一个线程有自己的CPU、寄存
器、堆栈。但是要和同一进程其他的线程共享资源(堆中的内存、文件等等)。跨线程
通讯通常是由异步调用来实现的:不是直接地调用函数,调用线程将包含有函数标示和
参数的结构体压入一个队列然后继续执行。另一个线程(被称为工作线程)取出队列中
的结构并利用其中的信息调用函数。将一个函数调用封装并执行的过程是非常直接和简
单的。然而,在一个异步调用多个函数的程序中,这可以引起代码膨胀。在这篇文章中
,我将给出一种减少代码膨胀并使异步调用更容易实施的方法。这个方法只是在运行时
引起一些间接调用的额外开销。
  这一机制是建立在原来的方法之上,所以,我将自下而上的解释我的解决方法。我
讲解是下面的选项:
  1. C方法,没有安全类型并且调用线程与工作线程之间存在紧耦合关系。
  2. 命令方法,使函数调用封装具有安全类型,并且降低调用线程和工作线程之间
的耦合关系。但是需要程序员为每一个异步调用函数实现一个类。
  3. 在2的基础上改善了功能子类模板,有效的减少了代码量。但是引入了比较晦涩
的语法结构,还有
  4. 在3的基础上,改善了创造函数模板,最终提供了易用性。
1) C方法
  对于使用面向对象的语言的工程来说这个再也不是一个实际的方法,但是我要给出
一个简短的说明以便这一事实在其他的方法中更加明显。在C中,队列的结构体中通常包
含有一个数来标志一个函数和一个指向另一个结构体void型的指针,这个结构体是一个
函数参数的封装器。利用这个数的数值,工作线程可以将一个void指针转型为正确的指
针来调用函数,并且将封装器的内容作为参数传入函数。
  非常明显,这里面有一个非常严重的问题:为了运行正确,调用函数必须精确的把
工作函数需要的压入队列。然而,由于在这里引入了转型,编译器不能帮助程序员在两
方面保持同步。例如,如果压入队列的数值和封装类型不搭配的话,工作线程极有可能
崩溃。此外,由于调用线程和工作线程之间的紧耦合关系,再引入一个新的函数的时候
两方都需要作出修改。下面的命令模式将解决上面的两个问题。
2)命令模式
  理想的情况下,工作线程应该对他执行的调用一无所知。命令模式通过提供一个定
义了最小的执行调用的接口的虚基类使这成为可能。一个实例如下:
    class Functor
   {
    public::
      virtual ~Functor() {}
      virtual void operator()() = 0;
   };
  每当你引入一个异步调用的函数的时候,你要做下面的一些事情:
  1. 从Functor类继承得到一个新类,这个类需要为每个参数提供一个成员变量
  2. 在派生类中在额外提供一个指向要执行成员函数的对象的指针。
  3. 在派生类中提供一个构造函数来通过参数初始化(或者拷贝构造)每一个成员
变量。
  4. 重载operator ()来调用函数,将成员变量作为参数。
  对于每一个函数调用,调用线程将做下面的事情:
  1. 为上述类分配一个新的对象。
  2. 将所有参数传入构造函数中。
  3. 将一个指向对象的指针入队。
  4. 恢复正常操作
  工作线程然后从队列中取出指向对象的指针,调用operator(),然后删除对象。
  如上面所说的,命令模式解决了C方式的最大的问题,然而,还有一个C问题在命令
模式中没有被解决:代码膨胀。在引入一个新的异步调用的时候,你都必须从Functor类
继承。在一个大程序中,你可能有这样数百个小类而终结。它们有相同的语法但是调用
的函数不同,函数的参数不同。如我下面所说的,C++模板非常适合来概括这些类的不同
点,并且消除手写代码来继承Functor。
3)功能子类模板
  首先看看下面从Functor类继承而来的模板类
  template< class CalleePtr, class
   MemFunPtr, class Parm1 >
  class MemberFunctor1 : public Functor
  {
   public:
    MemberFunctor1
    (
     const CalleePtr & pCallee,
     const MemFunPtr & pFunction,
     Parm1 aParm1
    ) :
     pCallee( pCallee ),
     pFunction( pFunction ),
     aParm1( aParm1 )
    {
    }
    virtual void operator()()
    {
     if ( ( pCallee != NULL ) &&
      ( pFunction != NULL ) )
     {
      ((*pCallee).*pFunction)(aParm1);
     }
    }
   private:
    CalleePtr pCallee;
    MemFunPtr pFunction;
    Parm1 aParm1;
   };
  请注意上面的operator (),我也可以写(pCallee->*pFunction ) ( aParm1 )来
完成调用函数指针,但是那样pCallee 不是普通的smart pointer了,因为大多数大多数
smart pointer不定义operator->*
  MemberFunctor1提供以一个包含一个参数的得通用解决方法。在http://www.cuj.c
om/code 上可以找到0-6个参数的C++函数类型,7个成员函数的类模板,7个函数和静态
成员。
  模板的用法如下:
  #include <iostream>
  class Foo
  {
    public:
     void Bar( const int & rVal ) { std::cout << rVal; }
   };
  int main( int argc, char* argv[] )
  {
    Foo aFoo;
    float aValue = 1.0;
    // really ugly!
    typedef MemberFunctor1
      < Foo *, void ( Foo::* )( const int & ), float >
      FooBarFunctor;
    Functor * pCall =
      new FooBarFunctor( &aFoo, &Foo::Bar, aValue );
    ++aValue;
    // the following would be done by the worker thread
    // pop call out of queue
    ( *pCall )();
    return 0;
  }
  这个看起来没太大希望,不是吗?特别是在typedef中的第二个模板参数的定义,一
个指向接受一个const int&返回void的成员函数Foo的指针,简直是语法的噩梦。我要在
一分钟内清除这些。因为我处理的是指向Functor的指针,工作线程用一些不常见的语法
(*pcall)()调用operator()。我可以将指针封装入一个句柄类,就像STL中的Functor
一样,但是因为在工作线程中只做一次调用,我决定坚持使用一些不方便的指针。
4)创造函数模板
  为了简化从类模板创建对象的过程,STL提供了像std::make_pair这样的模板函数,
std::make_pair模板需要两个类型如T,U作为参数来创建一个对象,形如std::pair<T,U
 >。他返回一个对象给调用者。std::make_pair的优点是不需要指明U或T--make_pair推
断出了传给它们的参数。
  我在下面给出相同类型的函数模板
   template< class CalleePtr, class Callee, class Ret,
     class Type1, class Parm1 >
   inline Functor * DeferCall
   (
     const CalleePtr & pCallee,
     Ret ( Callee::*pFunction )( Type1 ),
     const Parm1 & rParm1
   )
   { 
     return new
       MemberFunctor1< CalleePtr,
         Ret ( Callee::* )( Type1 ), Parm1 >
           ( pCallee, pFunction, rParm1 );
  }
  使用DeferCall,函数模板不需要显式的指明(也就是说,你不需要去填写模板参数
CalleePtr, Callee, Ret, Type1, 和 Parm1)。你可以像普通函数一样调用它们,编译
器回自动的将它们实例化:
   // the following line replaces the new
   // expression AND the ugly typedef
   Functor * pCall =
    DeferCall( &aFoo, &Foo::Bar, aValue );
一些实现细节:
  ● 三个参数传入了函数模板DeferCall。然后编译器推断出五个模板参数。这一过
程是可能的,因为函数指针组合了多种的类型,一个遵循标准的编译器可以做出这种推
断。
  ● 函数DeferCall最后一个的参数是float型变量,它的一份拷贝被储存在返回的对
象中。Float型和int型的转换发生在真正执行调用前。
  ● 类模板的名字描述了函数的类型(成员函数用一个参数调用)。因为由14个版本
的类模板它们必须都有一个唯一的名字。像普通函数一样,函数模板也可以被重载。所
以,创建函数模板可以被命名为DerferCall。你可以简单的传递对象,函数名和参数到
DeferCall,然后就是编译器对它们处理了。
  ● 成员函数指针需要是需要注意const的;就是说,我声明了一个成员函数Foo::B
ar为一个const函数,指向它的一个指针应该声明为void ( Foo::* )( const int & ) 
const (注意后面的const)所以,为了延迟调用const成员函数,定义另外一个函数模
板是必要的。每一个MemberFuctor模板类都是和两个DeferCall模板联合的。
  还有最后一个问题要解决:在例子的代码中,DeferCall的最后一个参数的一份拷贝
在封装器内被储存。函数Foo::Bar在延迟调用的时候用这份拷贝工作。这在一些时候不
是合适的。因为aValue在真正调用之前已经改变了,并且这里有必要使用最近更新的值

  有两个方法来解决这个问题,一个是一个相对简单的解决方法。它提供了对原有的
传递参数和对异步函数调用的传递参数规定的完全控制。第二种方法更具一般意义,它
没有这些限制,但是对语法要求苛刻一些。简单起见,我显讲述第一个方法:
   template< class Type >
   class Referencer
   {
    public:
     Referencer( Type & rType )
      : rType( rType ) {}
     operator Type &() const
     { return rType; }
    private:
     Type & rType;
   };
   template< class Type >
   inline Referencer< Type > Reference(
    Type & rType
   )
   {
    return Referencer< Type >( rType );
   }
  同时下面的代码用于生成一个延迟调用,Foo::Bar将用最近更新的值工作:
  Functor * pCall =
    DeferCall( &aFoo, &Foo::Bar,
      Reference( aValue ) );
  Referencer< float >保存着一个对传递进构造函数的float变量的引用。转换操作
符允许Referencer< float >隐式的转换为一个float引用。从这里,编译器生成一个in
t的临时int变量,最终这个临时变量传入Foo::Bar。
技术细节
  1. 用于函数模板的性质,没有参数的类型检查;就是说,DeferCall在相同的作用
域下不能够区分重载函数。如果你想延迟调用这些函数的话,编译器就会分不清楚。当
我在一个项目中引入相似的机制的时候,我担心用户用户会陷入太多这样的麻烦。然而
,我还没有收到任何的指责。我的结论是异步调用函数很少会被重载。
  2. 任何对于函数的生命都可能被忽略。你一直要做的是在传递给DeferCall和声明
一样多的参数。
  3. 另一个对于模板的关注是不正当的使用经常会引起编译时的错误。不熟悉模板
的程序员会经常的陷入错误之中,特别是在使用不支持返回信息的编译器的情况下。此
外,由于一些简单的类似函数的语法如DeferCall,不是很快就可知道引入了模板的。因
此,使用这种机制的程序员至少应该对模板有初步的了解。
  4. 考虑运行开销,对于函数指针的开销与队列的插入提取的同步来比几乎可以省
略,也不会成为一个问题。
  5. C++在执行转型的时候最多有一个用户类型。Reference已经有了一个用户定义
的类型转换操作符,所以传入Reference模板的变量必须是可以不用用户定义方法和函数
参数之间可以转换的。如前面所述,这一限制是可以避开的,这会使DeferCall的语法上
复杂一些。代替给Referencer一个转换操作符,增加一个成员函数Get,来返回储存的对
象。然后你建立一个模板类Copier和一个相应的模板函数Copy,它们都像他们的应用副本
一样。在所有的Fouctor的子模板类中在你需要传递一个或者访问一个参数用.Get()函数
。这样,所有函数参数都通过Reference或者Copier得到传递。
  6. 如果你的目标平台支持模板的特殊化,你可以摆脱上面所说的限制甚至不需要
知道Copy模板。我的平台不支持这一特性,所以我保留了它的可能性。
  7. 不同编译器对模板的支持情况迥异。所以,关于模板的第一个问题应当是?quo
t;他支持我的平台吗?"大部分情况是支持的,因为这一特性在STL函数模板mem_fun1中
出现,编译器提供商在STL上通常会花费很大的力气。本文的源码在Visual Studio 6.0
 下开发。
  8. 最后,模板通常因为编译码膨胀而受到指责。然而,如果不用到文中的方法,
程序员需要写编译器生成的所有代码。
结论
  借助于一行的C++代码,任何形式的函数调用可以置于一个Functor对象中。这是完
全的安全类型的实现,就是任何不搭配的错误将在编译是被指出。这一技术的最多应用
是在有线程通讯的多线程的程序中。调用线程用DeferCall包装调用并将Functor对象入
队。工作线程然后将对象出队,调用Functor::operator()并删除对象。
参考资料
[1] Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides. Design Patt
erns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1995).
[2] Andrei Alexandrescu. Modern C++ Design: Generic Programming and Design P
atterns Applied (Addison-Wesley, 2001). See the chapter, "Generalized functo
rs."
[3] Smart pointers with pointer-to-member operators are possible, but not ye
t very common. For a good discussion on this topic, see Scott Meyers' articl
e, "Implementing operator->* for Smart Pointers," Dr. Dobb's Journal, Octobe
r 1999. Also available at www.ddj.com/articles/1999/9910/9910b/9910b.htm.
[4] Bjarne Stroustrup. The C++ Programming Language, Third Edition (Addison-
Wesley, 1997), pp. 418-421.

--
※ 来源:·哈工大紫丁香 bbs.hit.edu.cn·[FROM: 202.118.170.172]
[百宝箱] [返回首页] [上级目录] [根目录] [返回顶部] [刷新] [返回]
Powered by KBS BBS 2.0 (http://dev.kcn.cn)
页面执行时间:3.712毫秒