C++11 成员函数作为pthread线程

我自己也觉得这个标题讲得云里雾里的。事情是这样的:很多时候,我们希望一个class的某个成员函数能够作为一个线程来执行,就像Python中的threading库,只要把Thread()方法的target参数指向一个成员方法,比如self.__run,那么self.__run方法就会成为一个线程执行代码段。而pthread_create的原型是这样的:

int pthread_create(pthread_t *thread,pthread_attr_t *attr,void *(*routine)(void *),void *arg);

注意第三个参数routine是一个普通函数,而不能是一个成员函数。这不是废话嘛,不是普通函数怎么传进去。虽然很清晰,但是有时会破坏面向对象的思想。比如说Python中这么一段逻辑:

import threading
import time

class A:

    def __init__(self,name):
        self.__name=name
        self.__count=0
        self.__running=True
        self.__thread=threading.Thread(target=self.__run)
        self.__thread.start()

    def count(self):
        return self.__count

    def stop(self):
        self.__running=False
        self.__thread.join()

    def __run(self):
        while self.__running:
            print(self.__name)
            self.__count+=1
            time.sleep(1)

a=A('zjs')
time.sleep(10)
a.stop()
print(a.count())

很显然,因为成员方法可以作为线程体来执行,所以获得了如下好处:

  • 线程的变量传递非常方便,直接读写成员变量即可;
  • 线程体作为对象的一部分,可以访问对象的私有变量和方法。

在C++11之前,如果要实现类似逻辑,一种写法是这样的:

A.h

#ifndef A_H
#define A_H

#include <pthread.h>

class A
{

public:
    A(const char* name);
    int count();
    void stop();

//不得不暴露
public:
    const char* m_name;
    int m_count;
    bool m_running;

private:
    pthread_t m_thread;

};

#endif

A.cpp

#include "A.h"

#include <stdio.h>
#include <unistd.h>

static void* __run(void* arg)
{
    A* a=(A*)arg;
    while(a->m_running)
    {
        printf("%s\n",a->m_name);
        a->m_count++;
        sleep(1);
    }
}

A::A(const char* name)
{
    m_name=name;
    m_count=0;
    m_running=true;
    pthread_create(&m_thread,0,__run,this);
}

int A::count()
{
    return m_count;
}

void A::stop()
{
    m_running=false;
    pthread_join(m_thread,0);
}

testA.cpp

#include "A.h"
#include <stdio.h>
#include <unistd.h>

int main()
{
    A a("zjs");
    sleep(10);
    a.stop();
    printf("%d\n",a.count());
    return 0;
}

注意到之所以需要把A的三个成员变量设为public,是为了能够让__run()能够访问到。但这就破坏了封装,使得这三个变量对外暴露。为了提高封装性,另一种好一些的办法是这样的:

A.h

#ifndef A_H
#define A_H

#include <pthread.h>

class A
{

public:
    A(const char* name);
    int count();
    void stop();
    //授权__A_run()能够访问私有变量
    friend void* __A_run(void* arg);

private:
    const char* m_name;
    int m_count;
    bool m_running;
    pthread_t m_thread;

};

#endif

A.cpp

#include "A.h"

#include <stdio.h>
#include <unistd.h>

void* __A_run(void* arg)
{
    A* a=(A*)arg;
    while(a->m_running)
    {
        printf("%s\n",a->m_name);
        a->m_count++;
        sleep(1);
    }
}

A::A(const char* name)
{
    m_name=name;
    m_count=0;
    m_running=true;
    pthread_create(&m_thread,0,__A_run,this);
}

int A::count()
{
    return m_count;
}

void A::stop()
{
    m_running=false;
    pthread_join(m_thread,0);
}

testA.cpp

#include "A.h"
#include <stdio.h>
#include <unistd.h>

int main()
{
    A a("zjs");
    sleep(10);
    a.stop();
    // 然而__A_run()本身对外暴露了
    //__A_run(&a);
    printf("%d\n",a.count());
    return 0;
}

可以看到,通过友元函数,可以使得线程体能够访问私有成员了。但是呢,友元函数本身是extern的,使得__A_run()本身对外暴露了,这使得外界可以手动调用__A_run,有一定的风险。不过函数暴露总归比变量暴露好得多。在C++11之前,封装性最好的办法,可能也就只能是这样了:在A.cpp中定义一个结构体,该结构体拥有和A一样的内存布局,并且A中成员变量连续分布,然后把A中第一个成员变量的地址传给线程体。比如这样:

A.h

#ifndef A_H
#define A_H

#include <pthread.h>

class A
{

public:
    A(const char* name);
    int count();
    void stop();

private:
    const char* m_name;
    int m_count;
    bool m_running;
    pthread_t m_thread;

};

#endif

A.cpp

#include "A.h"

#include <stdio.h>
#include <unistd.h>

//内存布局和A一样
struct A_vars
{
    const char* m_name;
    int m_count;
    bool m_running;
};

static void* __run(void* arg)
{
    struct A_vars* a=(struct A_vars*)arg;
    while(a->m_running)
    {
        printf("%s\n",a->m_name);
        a->m_count++;
        sleep(1);
    }
}

A::A(const char* name)
{
    m_name=name;
    m_count=0;
    m_running=true;
    //第一个变量的地址传过去
    pthread_create(&m_thread,0,__run,&m_name);
    //pthread_create(&m_thread,0,__run,this);
}

int A::count()
{
    return m_count;
}

void A::stop()
{
    m_running=false;
    pthread_join(m_thread,0);
}

这样做,对外接口确实完美了,但是!太依赖底层的细节了,如果编译器对内存分布进行了优化,或者A有虚表,这种方法很可能就爆炸了!换句话说,通过牺牲稳定性、可移植性来换取封装性,貌似不太可取。

好在C++ 11中出现了匿名函数,使得函数能够嵌套定义。

A.h

#ifndef A_H
#define A_H

#include <pthread.h>

class A
{

public:
    A(const char* name);
    int count();
    void stop();

private:
    void run();

private:
    const char* m_name;
    int m_count;
    bool m_running;
    pthread_t m_thread;

};

#endif

A.cpp

#include "A.h"

#include <stdio.h>
#include <unistd.h>

A::A(const char* name)
{
    m_name=name;
    m_count=0;
    m_running=true;
    pthread_create(&m_thread,0,
        [](void* arg)
        {
            A* a=(A*)arg;
            a->run();
            return (void*)0;
        }
    ,this);
}

int A::count()
{
    return m_count;
}

void A::stop()
{
    m_running=false;
    pthread_join(m_thread,0);
}

void A::run()
{
    while(m_running)
    {
        printf("%s\n",m_name);
        m_count++;
        sleep(1);
    }
}

代码一下子完美啦~~

编译A.cpp的时候,要这样:

g++ -std=gnu++11 -c A.cpp -o A.o