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

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

by adie
2006-03-30 16:54:52
    现在的 Win32 API 是 C 风格面向过程的(.NET 的 API 是完全面向对象的了),里面使用了很多回调函数来完成一些比较复杂的功能。其中创建线程就是其中之一。这在 C 语言里面调用是很方便的,但到了 C++ 面向对象风格的程序当中就不那么方便了。只能使用全局函数或静态类成员函数作为回调往往会影响软件的设计,破坏代码面向对象的风格。为此,大多数情况下我们都会选择一种迂回的方式来在线程中调用成员函数。这样的过程都大致相同,在代码中的多处出现时往往带来不必要的劳动,又破坏代码的简洁性。为此本文利用模板的方法为客户代码在创建线程时使用成员函数提供了简洁的方法,并最终封装为一个纯 .h 的库形式。

    要调用类的成员函数关键是提供一个传递 this 指针的方法。是的,我们可以用在全局变量或类静态变量中保存 this 指针,这样的代码看起来象这样:

class X;
X* g_this = NULL;

class X
{
public:
    X(){
        g_this = this;
    };

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

    int m_value;
};

void _cdecl thread_fun(void * arg)
{
    g_this->fun(arg);
}

int main()
{
    X obj;
    obj.m_value = 20;
    _beginthread(thread_fun, 0, (void*)10);
    system("PAUSE");
}

    恩,非常的糟糕,不光是代码不好看,里面还隐患重重。使用类静态变量来保存 this 指针也好不到哪儿去。所以这种方法显然并不可取。
    第二种方法是通过线程函数的参数来传递 this 指针,相信这也是我们实际使用的方法吧,这样的代码大致象这样:

class X
{
public:
    X(){};

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

    int m_value;

    static void _cdecl thread_fun(void * arg)
    {
        static_cast(arg)->fun();
    }

};

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

    这样看起来好多了,但是我们不得不为每一个要使用的线程函数 (fun) 创建一个 static 版本(thread_fun),非常的讨厌是吧? "懒惰是程序员的美德之一", 我们不能容忍这样的重复工作的!我们的第一步就是用模板来简化上面的过程。
    解决方法非常的简单,也很容易想到,我们可以提供一个这样的模板类:

template 
class ThAdp // Thread Function Adapter
{
public:
    static void __cdecl thread_fun(void *p)
    {
       (static_cast(p)->*MemFun)();
    }
};

    然后我们创建线程的过程就象这样:

class X
{
public:
    X(){};

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

    int m_value;
};

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

    省掉了类 X 中的 thread_fun 静态函数,没有任何效率或空间损失,一切都显得很完美。如果你觉得 ThAdp::thread_fun 这样的使用方法很繁琐,我们可以再定义一个宏:MEM_FUN(C, F) ThAdp::thread_fun 然后 _beginthread 可以这样调用 _beginthread(MEM_FUN(X, fun), 0, &obj); 很简洁了吧? “哦,不!”有人说道:“这里使用了宏!而且 MEM_FUN(X, fun) 也不符合习惯,我需要象 MEM_FUN(&X::FUN), MEM_FUN(&obj.fun) 这样调用!” 恩,这个问题以后再说,先来看看上面这个方法中的另一个问题,函数的参数。

    由于把线程函数的参数用来传递 this 指针了,我们用作线程函数的成员函数就没有参数了。这在某些情况下或许可以,但大多数情况下我们的函数还是有参数的。要给线程函数传参数其实也很简单,想想是怎么给全局的线程函数传多个参数的,我们只需要在这多个参数中使用一个来传递 this 指针就好了。于是有了如下一个模板类:

template 
class ThAdp1
{
public:
    struct ThreadArg{
        C* m_pThis;
        A  m_arg;

        ThreadArg(C* pThis, A arg): m_pThis(pThis), m_arg(arg){};
    };

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

    但是我们的客户端代码却象下面这个样子了:

class X
{
public:
    X(){};

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

    int m_value;
};

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

    _beginthread(ThAdp1::thread_fun, 0, &ThAdp1::ThreadArg(&obj, 10)) 这句调用确实不怎么样,甚至可以说让人望而生畏。下面我们试着来简化这个调用的方法吧。
    首先来看看对线程函数的参数 &ThAdp1::ThreadArg(&obj, 10) 的简化,这个看起来似乎比较容易,我们可以用一个模板函数让其中大多数的模板参数都可以推导出来,通过 &obj 参数可以推导出 C(Class) 的类型为 X,通过 10 可以推导出 A(Arg)的类型为 int,因此只需提供  void (C::*MemFun)(A) 值就可以了。使用了模板函数后的参数构造应该象这个样子: MakeAdp1Arg<&X::fun>(&obj, 10)。可是遗憾的是这行不通,因为第三个模板参数  void (C::*MemFun)(A) 不能象这样 template  放到前面来,这样会报错说 C 和 A 是未定义的类型。而如果 (C::*MemFun)(A) 放到最后的话即使前面两个类型可以推导出来,为了提供第三个模板参数也需要把前面两个模板参数一起填写了。
    对此,我想到了两种方法:第一种称为暴力法,类的成员函数指针其值无非是一个整形,我可以在传递的时候忽略其类型,进行强制转换。但是 C++ 对成员函数指针的类型控制非常严格,所以的转换操作符都不能对其转型,虽然用 union_cast 可以强行转,可是编译器认为这样的操作是在运行期完成的,不能用于模板参数。
    第二种方法是把 ThreadArg 结构拿到外面来,即模板定义改为:

template 
struct ThreadArg{
    C* m_pThis;
    A  m_arg;

    ThreadArg(C* pThis, A arg): m_pThis(pThis), m_arg(arg){};
};

template 
class ThAdp1
{
public:
    static void _cdecl thread_fun(void *p)
    {
        ThreadArg* pArg = static_cast* >(p);
        (pArg->m_pThis->*MemFun)(pArg->m_arg);
        delete pArg;
    }
};

    然后我们可以定义如下函数了:

template 
void * MakeThAdp1Arg(C* pThis, A arg)
{
    return new ThreadArg(pThis, arg);
}

   接着我们开始在客户代码种使用:

class X
{
public:
    X(){};

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

    int m_value;
};

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

    一切看起来都很顺利,最后的效果 MakeThAdp1Arg(&obj, 10) 也很简洁,似乎问题已经解决了,可以回去休息了。哦,等一等,这里面存在一个非常严重的隐患,考虑这样的客户代码:

_beginthread(ThAdp1::thread_fun, 0, MakeThAdp1Arg(&obj, "Hello World"));

    顺利通过了编译,但运行的时候会发生什么呢? 哦,没有人知道。
    或许你认为这样的错误没有人会犯的,那这样的错误呢:

int main()
{
    X obj;
    obj.m_value = 20;
    char c = 10;
    _beginthread(ThAdp1::thread_fun, 0, MakeThAdp1Arg(&obj, c));
    system("PAUSE");
}

    大家都会认为 char 都 int 会被自然提升的,于是这样用也是理所当然。但是非常抱歉,你的程序行为现在是未定义的,没有比这更糟的了。
    由此可看出,这种解决方法也有严重的缺陷,是不可取的,我们必须得让编译器为我们做类型检查。那如何才能实现即简洁又安全的代码呢? 呵呵,下回分解。

(待续...)

附:本文实例代码都在 VC7.1 上测试进行的,如在其他编译器上有问题,欢迎留言或 email 指出。

▲评论

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


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