【muduo】base库之 Thread

一、基础知识

POSIX线程(英语:POSIX Threads,常被缩写为Pthreads)是POSIX 的线程标准,定义了创建和操纵线程的一套API。

实现POSIX 线程标准的库常被称作Pthreads,一般用于Unix-likePOSIX 系统,如Linux、Solaris。它以pthread.h头文件和一个线程库实现。Pthreads API中大致共有100个函数调用,全都以"pthread_"开头。

  • pthread_t 是线程句柄,用于声明线程ID。 类型定义:typedef unsigned long int pthread_t;
  • pid_t  是Linux下的进程号类型,也就是Process ID _ Type 的缩写。其实是宏定义的unsigned int类型,在Linux下线程是一个轻量级进程,也有自己的进程ID是(每个线程唯一),可以通过系统调用::syscall(SYS_gettid)获取。

Thread即有pthread_t 也有pid_t  ,它们各有用处,pthread_t 给pthread_XXX函数使用,而pid_t作为线程标识。

 

pthread_join

函数定义: int pthread_join(pthread_t thread, void **retval);

参数 :thread: 线程标识符,retval: 用户定义的指针,用来存储被等待线程的返回值。

返回值 : 0代表成功。 失败,返回的则是错误号。

       代码中如果没有pthread_join 主线程会很快结束从而使整个进程结束,从而使创建的线程没有机会开始执行就结束了。加入pthread_join后,主线程会一直等待直到等待的线程结束自己才结束,使创建的线程有机会执行。

 

int pthread_join(pthread_t th, void **thread_return)

pthread_join()的调用者将挂起并等待th线程终止,retval是pthread_exit()调用者线程(线程ID为th)的返回值,需要注意的是一个线程仅允许唯一的一个线程使用 pthread_join()等待它的终止。

 

pthread_create

pthread_create是类Unix操作系统(Unix、Linux、Mac OS X等)的创建线程的函数。它的功能是创建线程(实际上就是确定调用该线程函数的入口点),在线程创建以后,就开始运行相关的线程函数。

pthread_create的返回值 表示成功,返回0;表示出错,返回-1。

#include 
int pthread_create(
                 pthread_t *restrict tidp,   //新创建的线程ID指向的内存单元。
                 const pthread_attr_t *restrict attr,  //线程属性,默认为NULL
                 void *(*start_rtn)(void *), //新创建的线程从start_rtn函数的地址开始运行
                 void *restrict arg //默认为NULL。若上述函数需要参数,将参数放入结构中并将地址作为arg传入。
                  );

参数

第一个参数为指向线程标识符的指针。

第二个参数用来设置线程属性。

第三个参数是线程运行函数的起始地址。

最后一个参数是运行函数的参数。

 

 

 

 

pthread_atfork()

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

pthread_atfork()在fork()之前调用,当调用fork时,内部创建子进程前在父进程中会调用prepare,内部创建子进程成功后,父进程会调用parent ,子进程会调用child。

实际上对于编写多线程程序来说最好不要调用fork() 即不要编写多线程多进程程序 要么用多进程要么用多进程 因为多线程fork()之后容易出现死锁。

fork 可能是在主线程中调用,也可能是在子线程中调用,fork得到一个新进程,新进程只有一个执行序列,只有一个线程(也就是新进程中的主线程,所以需要把线程名改为main)(调用fork的线程被继承下来)

 

snprintf()函数:

int snprintf ( char * str, size_t size, const char * format, ... );

参数

  • str -- 目标字符串。
  • size -- 拷贝字节数(Bytes)。
  • format -- 格式化成字符串。
  • ...  -- 可变参数。

 

基于对象和面向对象

面向对象依靠的是虚函数+继承,用户通过重写基类的虚函数,实现自己的逻辑。

基于对象,依赖类的组合,使用function和bind实现委托机制,更加依赖于回调函数传入逻辑。

muduo库使用的是基于对象的编程思想,只暴露具体类不暴露抽象类,这在Thread类的设计中就得到体现。

如果用面向对象的编程风格封装 Thread 类,则是利用虚函数+继承的方式,通过重写Thread基类的run方法,传入自己的用户逻辑。

在muduo库中采用 C++11的function,将函数作为Thread类的成员,用户只需要将function对象传入线程即可,所以Thread的声明中,应该含有一个function成员变量。

 

std::function 使用

将其当做std::string 、std::vector<>、这样的类型就可以了。只不过其值为函数指针,但比函数指针更灵活。

因为std::function 是一种模板,所以要传入类型,就如std::vector传入类型 int 一样

不过,std::function传入的是函数类型  返回值 (参数类型) 如:std::function
 

 

 

 

explicit关键字

C++中,explicit关键字只能用于修饰只有一个参数的类构造函数 (或者除了第一个参数外其余参数都有默认值的多参构造函数), 它的作用是表明该构造函数是显示的, 而非隐式的。如果类构造函数参数大于或等于两个时, 是不会产生隐式转换的, 所以explicit关键字也就无效了。

class Test1
{
public:
    Test1(int n)
    {
        num=n;
    }//普通构造函数
private:
    int num;
};
class Test2
{
public:
    explicit Test2(int n)
    {
        num=n;
    }//explicit(显式)构造函数
private:
    int num;
};
int main()
{
    Test1 t1=12;//隐式调用其构造函数,成功
    Test2 t2=12;//编译错误,不能隐式调用其构造函数
    Test2 t2(12);//显式调用成功
    return 0;
}

 

PID与TID,Linux的进程和线程

  • 进程是资源分配的基本单位,线程是调度的基本单位
  • 进程是资源的集合,这些资源包括内存地址空间,文件描述符等等,一个进程中的多个线程共享这些资源。
  • CPU对任务进行调度时,可调度的基本单位 (dispatchable entity)是线程。如果一个进程中没有其他线程,可以理解成这个进程中只有一个主线程,这个主进程独享进程中的所有资源。
  • 进程的个体间是完全独立的,而线程间是彼此依存,并且共享资源。多进程环境中,任何一个进程的终止,不会影响到其他非子进程。而多线程环境中,父线程终止,全部子线程被迫终止(没有了资源)。

 

       线程之间虽然共享地址空间,但是操作系统仍然赋予了她们一部分专属于自己的空间,用于区分它们,比如线程的tid,这里明确一下操作系统中和进程线程有关的几个id变量。

  • pid,每个进程的id,类型为pid_t,可以通过getpid()取得。
  • 线程id,类型为pthread_t,这个id在每个进程里是唯一的,它只用于在进程里区分某个线程,也就是说不同的进程里可能有两个线程的线程id是一样的。可以由pthread_self()取得。
  • tid,线程的真实id,操作系统保证这个值对于每个线程是唯一的,也就是说进程1下面的线程要和进程2下面的线程沟通,只能使用tid。这个值只能通过linux的系统调用取得,syscall(SYS_gettid)
     

为什么使用pid_t而不使用pthread_t来标识线程id呢?

pthread_t的值很大,无法作为一些容器的key值。 glibc的Pthreads实现实际上把pthread_t作为一个结构体指针,指向一块动态分配的内存,但是这块内存是可以反复使用的,也就是说很容易造成pthread_t的重复。也就是说pthreads只能保证同一进程内,同一时刻的各个线程不同;不能保证同一个进程全程时段每个线程具有不同的id,不能保证线程id的唯一性。

在LINUX系统中,建议使用gettid()系统调用的返回值作为线程id,这么做的原因: 
返回值是一个pid_t,其值是一个很小的整数,方便输出。 
在linux系统中,它直接标识内核任务调度id,可通过/proc文件系统中找到对应项:/proc/tid 或者 /proc/pid/task/tid,方便定位到具体线程。任何时刻都是唯一的,并且由于linux分配新的pid采用递增轮回办法,短时间内启动多个线程也会具有不同的id。

 

 

 

关键字:__thread
线程共享进程的数据,如果想要每个线程都有一份独立的数据,那么可以使用__thread关键字修饰数据

通过使用__thread类型的变量,上面的数据在每个线程中就都有一份而且没有相互覆盖的情况,这个地方如果不知道这种做法的只能通过一个全局的数组来记录了,操作的时候还需上锁,既影响效率还麻烦容易出错。

 

extern 关键字

extern也可用来进行链接指定;也可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。

说人话,即有以下两个作用:

  • 与“C”连用,如extern “C” void fun();告诉编译器按C的规则去翻译
  • 修饰声明全局变量或函数,其声明的变量和函数可以在其它模块(文件)中使用,注意,这只是一个声明而不是定义,具体的定义要在具体的模块中完成

例子

举个例子,我们想要声明一个long int runtime_minute变量供多个模块使用:

//time.h
#ifndef TIME_H
#define TIME_H
//首先我们在time.h中进行以下声明:
extern long int runtime_minute;
#endif //TIME_H

在头文件中通过extern声明的变量并没有定义,所以要在time.cpp中给出定义:

//time.cpp
#include "time.h"
//在time.cpp中给出runtime_minute变量的具体定义:
long int runtime_minute = 0;

这时,如果想要在time2.cpp中使用这个定义在time.cpp中的runtime_minute变量,只需要包含time.h这个头文件即可:

//time2.cpp
#include "time.h"
#include 
//在time.cpp中给出runtime_minute变量的具体定义:
int main(){
    runtime_minute++;
    std::cout<

 

C++ assert()函数用法

其作用是如果它的条件返回错误,则终止程序执行。

原型定义:

#include 
void assert( int expression );

 assert的作用是先计算表达式 expression ,如果其值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用 abort 来终止程序运行。

 

二、Thread类

muduo库中线程的封装是采用基于对象实现的,首先,为了获得最干净的语义,Thread应该是不可复制的,所以需要继承NonCopyable。

Tid表示线程的真实id。 POSIX的thread库提供了pthread_self获取当前线程的标识符,类型为pthread_t,这是一个结构体,用起来不便。线程的真实id是一个整数,可以通过系统调用syscall(SYS_gettid)获得,在muduo中封装为gettid()函数。调用系统调用开销比较大,因此可以使用一个变量t_cachedTid来存储,在线程第一次使用tid时通过系统调用获得,存储在t_cacheTid中,以后使用时不再需要系统调用了。

muduo库thread.cc代码分为两部分,detail命名空间部分是实现是用的一些全局函数和数据结构,Thread::部分是线程成员函数的实现,

 

为什么不能直接在创建线程的时候执行某个类的成员函数?

因为pthread_create 的线程函数定义为void *func(void*),无法将 non-staic 成员函数传递给pthread_create 。

 

startThread 是一个外部的函数,用来调用 ThreadData 的 runInThread ,因为 pthread_create 只接受静态函数,所以我们需要一个跳板,这便是 startThread 存在的意义。runInThread 里我们修改线程名字,获取线程 tid 的值,运行 fun 。

 

 

 

三、CurrentThread命名空间

1、说明

      这是一个命名空间,不是类,主要是线程类和异常类中需要用到这个命名空间。thread的封装和一个命名空间muduo::CurrentThread紧密相关,在这个空间中定义了和线程相关的属性

   

2、变量说明

   以下所有变量都是__thread修饰的,这样的变量是线程局部存储的,也就是说每个线程都有个独立的变量

  1. t_cachedTid
    • 线程真实pid缓存
    • 如果每次都调用getpid()获得pid,效率较低
  2. t_tidString
    • 这是tid的字符串表示形式
  3. t_tidStringLength
    • 如名,长度
  4. t_threadName
    • 如名,名字

t_cachedTid 是线程真实pid的缓存,为什么要做缓存呢?因为调用系统调用开销比较大,因此可以使用一个变量t_cachedTid来存储,在线程第一次使用tid时通过系统调用获得,存储在t_cacheTid中,以后使用时不再需要系统调用了。

3. 函数说明

1. 内联

tid()
如过是第一次调用,调用cacheTid(),再调用detail::gettid(),再调用系统的SYS_gettid,获得线程tid赋值给t_cacheTid,也就是获得当前线程id并缓存
返回t_cacheTid的值


tidString()
返回t_tidString


tidStringLength()
返回t_tidStringLength


name()
返回t_threadName
 


 

你可能感兴趣的:(muduo源码剖析)