A!die Software Studio Welcome to A!Die Software Studio

如何用类成员函数作为线程函数(二)

by adie
2006-04-04 13:25:39
三、带两个参数的成员线程函数

    上一次我们实现了用不带参数的类成员函数和带一个参数的类成员函数来用做线程函数。但在调用方法的简化上碰到了一些问题,这里先把这些问题放一放,来看看带两个参数的成员函数作为线程函数。
    虽然说带一个参数的函数已经可以完成所有的需要了,因为函数的参数类型是泛型的,可以是一个结构体,一个指针,可以通过它传递任意多的参数了。但是在传递多个参数是必须定义额外的结构来传递,有些麻烦。为此,我们可以把这些细节封装起来,给客户代码提供直接传递多个参数的方法。
    在传递一个参数的成员线程函数的基础上,很容易实现了传递两个参数的成员线程函数:

template 
class ThAdp2
{
public:
    struct ThreadArg{
        C* m_pThis;
        A1  m_arg1;
        A2  m_arg2;

        ThreadArg(C* pThis, A1 arg1, A2 arg2): m_pThis(pThis), m_arg1(arg1), m_arg2(arg2){};
    };

    static void _cdecl thread_fun(void *p)
    {
        ThreadArg* pArg = static_cast(p);
        (pArg->m_pThis->*MemFun)(pArg->m_arg1, pArg->m_arg2);
    }
};

    客户代码使用方法:

class X
{
public:
    X(){};

    void fun(int x, int y)
    {
        printf("class member: %d, arg: %d, %d", m_value, x, y);
    }

    int m_value;
};

int main()
{
    X obj;
    obj.m_value = 20;
    _beginthread(ThAdp2::thread_fun, 0, &ThAdp2::ThreadArg(&obj, 10, 20));
    system("PAUSE");
}

    好了,现在我们来看看已经实现了的这三种调用方法:
不带参数:_beginthread(ThAdp::thread_fun, 0, &obj);
一个参数:_beginthread(ThAdp1::thread_fun, 0, &ThAdp1::ThreadArg(&obj, 10));
三个参数:_beginthread(ThAdp2::thread_fun, 0, &ThAdp2::ThreadArg(&obj, 10, 20));
    对不同参数的调用方法我们得使用不同的类: ThAdp, ThAdp1, ThAdp2 ... 虽然我们可以定义一些宏来简化这些调用方法,但对不同参数个数的调用也得使用不同的宏。这算不上什么缺陷,但是非常的不方便。如果能把这些调用方法都统一起来就好了,这是我们自然会想到函数重载,函数重载的设计目的就是为了对不同的参数以相同的形式调用。

四、不同个数参数的统一调用形式

    首先我们来看看线程函数部分:
      ThAdp::thread_fun
      ThAdp1::thread_fun
      ThAdp2::thread_fun
    要实现函数重载来提供统一的形式,必须为函数提供不同的参数。我们当然希望函数能够接收 &X::fun 这个参数,这样类的类型和函数的参数类型都可以推导出来了,无需显示指出。以两个参数为例,如果能实现这样的代码当然很好:

typedef void (_cdecl *ThreadFun_t)(void *);

template 
ThreadFun_t MakeThreadFun(void (C::*mem_fun)(A1, A2))
{
    return ThAdp2::thread_fun;
}

    这样我们的第一个参数就可以全部统一为 MakeThreadFun(&X::fun), 遗憾的是这行不通,因为尽管我们知道传递给函数的 mem_fun 是一个编译期常量,但编译器无法知道,mem_fun 作为函数参数已经是一个运行时的变量了,我们无法将其传递给 ThAdp2 作为模板参数。
    虽然在设计函数时也可以把 mem_fun 作为一个模板参数,象下面这样:

template 
ThreadFun_t MakeThreadFun(void (C::*)(A1, A2))
{
    return ThAdp2::thread_fun;
}

    这样确实可行,但我们的函数调用就不再简洁了,变成了 MakeThreadFun(&X::fun)。这当然非常不爽,我们希望象 MakeThreadFun(&X::fun) 这样简洁的方法来调用。
    为了实现这个目的,我们必须作出一个妥协,一个效率和简洁的妥协。为了简洁性,我们得把成员函数的指针作为一个运行期量来存储和使用。这需要一个额外的空间来存储这个指针,并在调用的时候来确定这个值。我想相对于开启一个线程的开销来说这是微不足道的(开线程需要内核调用,分配堆栈等,开销比一次普通的函数调用大得多)。
    函数的指针只能存储在传递的参数中,为此我们得修改一下我们的模板,这里以不带参数和两个参数为例来说明:

template 
class ThAdp0
{
public:
    typedef void (C::*MemFun_t)();
    
    struct ThreadArg
    {
        C* pThis;
        MemFun_t pFun;
    };

    static void _cdecl thread_fun(void *p)
    {
        ThreadArg* pArg = static_cast(p);
        C* pThis = pArg->pThis;
        MemFun_t Fun = pArg->pFun;
        (pThis->*Fun)();
        delete pArg;
    }
};

template 
class ThAdp2
{
public:
    typedef void (C::*MemFun_t)(A1, A2);

    struct ThreadArg
{
        C* pThis;
        MemFun_t pFun;
        A1 arg1;
        A2 arg2;
    };

    static void _cdecl thread_fun(void *p)
    {
        ThreadArg* pArg = static_cast(p);
        C* pThis = pArg->pThis;
        MemFun_t Fun = pArg->pFun;
        (pThis->*Fun)(pArg->arg1, pArg->arg2);
        delete pArg;
    }
};

    这里把成员函数的指针存储到了传递的参数中,模板类就不需这个模板参数了。但为了用成员函数来推导类和参数的类型,我们定义的函数也接受一个成员函数指针作为参数,虽然这个参数并不会被使用:

typedef void (_cdecl *ThreadFun_t)(void *);

template 
ThreadFun_t MakeThFun(void (C::*)())
{
    return ThAdp0::thread_fun;
}

template 
ThreadFun_t MakeThFun(void (C::*)(A1, A2))
{
    return ThAdp2::thread_fun;
}

    好了,现在不管成员函数是什么样子,我们开启线程是传递线程函数都可以使用 MakeThreadFun(&X::fun) 的形式了。根据重新定义的模板,我们很容易把构造参数的过程也用函数重载的方法来简化了:

template 
void* MakeThArg(C* pThis, void (C::*pMemFun)())
{
    ThAdp0::ThreadArg* pArg = new ThAdp0::ThreadArg;
    pArg->pThis = pThis;
    pArg->pFun = pMemFun;
    return pArg;
}

template 
void* MakeThArg(C* pThis, void (C::*pMemFun)(A1, A2), A1 arg1, A2 arg2)
{
    ThAdp2::ThreadArg* pArg = new ThAdp2::ThreadArg;
    pArg->pThis = pThis;
    pArg->pFun = pMemFun;
    pArg->arg1 = arg1;
    pArg->arg2 = arg2;
    return pArg;
}

   现在我们可以来看看我们客户代码的调用形式了:

class X
{
public:
    X(){};

    void fun()
    {
        printf("class member: %d, arg: %d ", m_value, 0);
    }

    void fun2(int x, const char * str)
    {
        printf("class member: %d, arg1: %d, arg2: %s ", m_value, x, str);
    }

    int m_value;
};

int main()
{
    X obj;
    obj.m_value = 20;
    _beginthread(MakeThFun(&X::fun), 0, MakeThArg(&obj, &X::fun));
    _beginthread(MakeThFun(&X::fun2), 0, MakeThArg(&obj, &X::fun2, 10, "Hello"));
    system("PAUSE");
}

    恩,象 _beginthread(MakeThFun(&X::fun), 0, MakeThArg(&obj, &X::fun));  _beginthread(MakeThFun(&X::fun2), 0, MakeThArg(&obj, &X::fun2, 10, "Hello")); 这样的调用形式确实非常简洁了。但是上面的方法还存在两个问题:

    1、参数类型检查过于严格了,象下面这样的代码已经无法通过编译:
char c = 10;
_beginthread(MakeThFun(&X::fun2), 0, MakeThArg(&obj, &X::fun2, c, "Hello")); 
    这样 MakeThArg 函数在推导 A1 这个类型的时候,根据 &X::fun2 推导应该是 int 型,根据 c 这个参数推导应该是 char 型,于是编译器无法确定模板的参数到底是什么类型了。解决的办法是显示指明参数的类型,即象这样:MakeThArg(&obj, &X::fun2, c, "Hello") 我想这应该算不上麻烦,并且只有在类型不同的时候才需要。这也是定义模板函数是把函数参数类型放到前面,把类类型放到后面的原因,为了必须显示指明参数的时候方便一些。
   2、第二个问题是象下面这种调用产生的:
   _beginthread(MakeThFun(&X::fun), 0, MakeThArg(&obj, &X::fun2, 10, "Hello"));
   即生成线程函数和参数时所使用的函数不同,前面使用了 &X::fun, 而后面使用的却是 &X::fun2。这固然是由于客户代码的粗心造成的,但是我们提供的方法使编译器无法对此做任何提示。这个问题比上面一个问题严重,因为对上面一个问题编译器会做出提示,无法继续。这个问题的后果只有等到运行期程序崩溃了,这还是好的,如果两个函数的参数恰好相同,那程序可能不会出错,但行为诡异,哦,没有什么比这更遭的了。
   或许我们可以忽略这个问题,因为原来的全局的线程函数和参数类型之间也无法做出安全型的检查,如果这两个参数不对应也会出现程序未定义的行为,这完全应该是客户代码的责任。但是我们可以做得更好的,具体方法请看下文。

(待续...)

▲评论

› 网友 匿名 () 于 2006-05-25 22:43:17 发表评论说:

阿呆,我觉得你不如使用现成的funtor库
boost/function
或者loki/functor
大概看了一下你的代码,基本上就是一个对仿函数的又一次封装....

› 网友 adie (webmaster@adintr.com) 于 2006-06-05 23:05:11 发表评论说:

可是系统的函数并不接受 boost/function 或是 loki/functor 做为参数。使用这些库可能会使上面的某些代码简化(不是代替),不过又增加额外的依赖性,我也不清楚在内部使用这些会不会增加额外的开销。
顺便提一下,上面的代码并非是我解决这个问题的最终方法。

X 正在回复:
姓 名: 留下更多信息
性 别:
邮 件:
主 页:
Q Q:
来 自:
职 业:
评 论:
验 证:


Valid HTML 4.01 Strict Valid CSS!
Copyleft.A!die Software Studio.ADSS
Power by webmaster@adintr.com