muduo网络库学习笔记(3):Thread类

muduo网络库采用了基于对象的编程思想来封装线程类。

类图如下:
muduo网络库学习笔记(3):Thread类_第1张图片
变量numCreated_表示创建的线程个数,类型为AtomicInt32,用到了我们上篇所说的原子性操作。
Thread类中还用到了CurrentThread类。

代码要点如下:

(1)线程标识符
Linux中,每个进程有一个pid,类型为pid_t,由getpid()取得。Linux下的POSIX线程也有一个id,类型为pthread_t,由pthread_self()取得,该id由线程库维护,其id空间是各个进程独立的(即不同进程中的线程可能有相同的id)。Linux中的POSIX线程库实现的线程其实也是一个进程(LWP:轻量级进程),只是该进程与主进程(启动线程的进程)共享一些资源而已,比如代码段,数据段等。

有时候我们可能需要知道线程的真实pid。比如进程P1要向另外一个进程P2中的某个线程发送信号时,既不能使用P2的pid,更不能使用线程的pthread id,而只能使用该线程的真实pid,称为tid
函数gettid()可以得到tid,但glibc并没有实现该函数,只能通过Linux的系统调用syscall来获取。

return syscall(SYS_gettid)

因为使用系统调用开销很大,所以我们需要对所获取的tid做一个缓存,防止每次都使用系统调用,从而提高获取tid的效率。

代码片段:缓存tid
文件名:CurrentThread.h

......

extern __thread int t_cachedTid;  // 线程真实pid(tid)的缓存

......

inline int tid()
{
    if (t_cachedTid == 0)
    {
      cacheTid();
    }
    return t_cachedTid;
}
代码片段:cacheTid()
文件名:Thread.cc

void CurrentThread::cacheTid()
{
  if (t_cachedTid == 0)
  {
    t_cachedTid = detail::gettid();
    int n = snprintf(t_tidString, sizeof t_tidString, "%5d ", t_cachedTid);

    // (void) n; 的用法是为了防止未使用变量n而出现编译错误
    assert(n == 6); (void) n;
  }
}

(2)__thread关键字和POD类型
__thread是GCC内置的线程局部存储设施,存取效率可以和全局变量相比。__thread变量在每一个线程有一份独立实体,各个线程的值互不干扰。可以用来修饰那些带有全局性且值可能变,但是又不值得用全局变量保护的变量。用一个例子来理解它的用法。

#include 
#include 
#include 

using namespace std;

//__thread int var = 5;
int var = 5;

void *worker1(void* arg);
void *worker2(void* arg);

int main()
{
        pthread_t p1, p2;

        pthread_create(&p1, NULL, worker1, NULL);
        pthread_create(&p2, NULL, worker2, NULL);
        pthread_join(p1, NULL);
        pthread_join(p2, NULL);

        return 0;
}

void *worker1(void* arg)
{
        cout << ++var << endl;
}

void *worker2(void* arg)
{
        cout << ++var << endl;
}

/**
 * 使用__thread关键字,输出为: 
 *                         6
 *                         6
 * 
 * 不使用__thread关键字,输出为:
 *                         6   
 *                         7
 * /

**注:**__thread只能修饰POD类型,不能修饰class类型,因为无法自动调用构造函数和析构函数。

POD类型(plain old data)是指与C兼容的原始数据类型,例如,结构体和整型等C语言中的类型就是 POD 类型,但带有用户定义的构造函数或虚函数的类则不是:

__thread可以用于修饰全局变量、函数内的静态变量,但是不能用于修饰函数的局部变量或者class的普通成员变量。

另外,__thread变量的初始化只能用编译器常量。

__thread string t_obj1(“hello”);    // 错误,不能调用对象的构造函数
__thread string* t_obj2 = new string;   // 错误,初始化必须用编译期常量
__thread string* t_obj3 = NULL; // 正确,但是需要手工初始化并销毁对象

(3)pthread_atfork()函数

#include 
int pthread_atfork(void (*prepare)(void), 
                    void (*parent)(void), 
                    void (*child)(void));

用法:调用fork时,内部创建子进程前在父进程中会调用prepare,内部创建子进程成功后,父进程会调用parent ,子进程会调用child。

(4)多线程与fork()
对于编写多线程程序来说,最好不要再调用fork(),即不要编写多线程多进程程序。因为Linux的fork()只克隆当前线程的thread of control ,不克隆其他线程。fork()之后,除了当前线程之外,其他线程都消失了,也就是说,不能一下子fork()出一个和父进程一样的多线程子进程。

fork()之后子进程中只有一个线程,其他线程都消失了,这就造成一个危险的局面。其他线程可能正好位于临界区之内,持有了某个锁,而它突然死亡,再也没有机会去解锁了。如果子进程试图再对同一个mutex加锁,就会立刻死锁。

一个在多线程程序里fork造成死锁的例子:

/*
 死锁的原因:
 1. 线程里的doit()先执行
 2. doit执行的时候会给互斥量mutex加锁
 3. mutex的内容会原样拷贝到fork出来的子进程中(在此之前,mutex变量的内容已经被线程改写成锁定状态)
 4. 子进程再次调用doit的时候,在给互斥量mutex加锁的时候会发现它已经被加锁,所以就一直等待,直到拥有该互斥体的进程释放它(实际上没有人拥有这个mutex锁)
 5. 线程的doit执行完成之前会把自己的mutex释放,但这时的mutex和子进程里的mutex已经是两份内存.所以即使释放了mutex锁也不会对子进程里的mutex造成什么影响
*/

#include 
#include 
#include 
#include 

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void* doit(void* arg)
{
    printf("%d begin doit\n",static_cast<int>(getpid()));
    pthread_mutex_lock(&mutex);
    struct timespec ts = {2, 0};
    nanosleep(&ts, NULL);
    pthread_mutex_unlock(&mutex);
    printf("%d end doit\n",static_cast<int>(getpid()));

    return NULL;
}

int main(void)
{
    printf("%d enter main\n", static_cast<int>(getpid()));
    pthread_t tid;
    pthread_create(&tid, NULL, doit, NULL);
    struct timespec ts = {1, 0};
    nanosleep(&ts, NULL);
    if (fork() == 0)
    {
        doit(NULL);
    }
    pthread_join(tid, NULL);
    printf("%d exit main\n",static_cast<int>(getpid()));

    return 0;
}

运行结果如下:
muduo网络库学习笔记(3):Thread类_第2张图片

你可能感兴趣的:(muduo)