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毫秒