7.1线程2015/8/2

线程概念

线程和进程的关系

1.轻量级进程,也有PCB,创建线程使用的底层函数和进程一样,都是clone。
2.从内核里看进程和线程是一样的,都有各自不同的PCB(但进程id号是一样的),但是PCB指向内存资源的三级页表是相同的(共用了地址空间)。
3.进程可以蜕变成线程(一个a.out进程,运行时分离出一个线程,那原来的a.out也就蜕变成线程了)。
4.线程就是寄存器和栈。
5.在linux下,线程是最小的执行单位(调度单位);进程是最小的分配资源单位(0~4G地址空间,时间片)。

系统是给进程分配0~4G地址空间,如果同一个进程分出线程,PCB指向的三级页表是相通的,

ps -Lf pid 查看这个进程里面有多少线程 LWP轻量级进程id
ps -eLf 查看所有

线程间共享资源

1.文件描述符表
2.每种信号的处理方式
3.当前工作目录
4.用户ID和组ID
5.内存地址空间
    TEXT(代码段)、data(已初始化全局变量)、bss(未初始化全局变量)、堆、共享库
    栈不共享

栈的管理方式:CPU中有个ESP栈指针寄存器,占四个字节,用来保存地址,通过地址的位移来操作栈空间
              PC程序计数器,也是四个字节,用来保存地址,这个保存的是CPU给到代码段的那个位置取指令的地址

PCP里面有内核栈,在保存处理器现场的时候,用来保存每个线程分别的ESP和PC,使得线程之间可以正常切换。

线程间非共享资源

1.线程id
2.处理器现场和栈指针(内核栈)
3.独立的栈空间(用户空间栈)
4.errno变量
5.信号屏蔽字
6.调度优先级

线程优缺点

优点
    提高程序的并发性
    开销小,不用重新分配内存
    通信和共享数据方便
    线程切换比进程块,不用重新映射0~4G的地址空间
缺点
    线程不稳定(库函数实现)
    线程调试比较困难(gdb支持不好)
    线程无法使用unix经典事件 例如信号。

查看manpage关于pthread的函数
man -k pthread
安装pthread相关manpage
sudo apt-get install manpages-posix manpages-posix-dev

线程原语

pthread_create

创建线程
    #include <pthread>
    int pthread_create(pthread_t *tread, const pthread_attr_t *attr,
                    void *(*start_routine)(void *), void *arg);
    pthread_t *thread:传递一个pthread_t变量地址进来,用于保存新线程的tid(线程ID)
    const pthread_attr_t *attr: 线程属性设置, 如使用默认属性,则传NULL
    void *(*start_routine)(void *):函数指针,指向新线程应该加载执行的函数模块
    void *arg:指定线程将要家在调用的那个函数的参数
    返回值:成功返回0,失败返回错误号。

    gcc编译的时候要加上 -lpthread

pthread_self

获取调用线程tid(进程内部有效)
    pthread_t pthread_self(void);

例子:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>

struct STU {
    int id; 
    char name[20];
};

void *thread_do(void *arg)  //线程的执行函数
{
    struct STU *s = (struct STU *)arg;
    while(1) {
        printf("id = %d, name = %s, tid = %x\n", s->id, s->name, (int)pthread_self()); 
        //打印结构体的内容,和本线程的tid
        sleep(1);
    }       
}

int main(void)
{
    pthread_t tid;
    struct STU student = {12, "xiaoming"};
    int err;

    err = pthread_create(&tid, NULL, thread_do, (void *)&student);//创建线程,线程的执行函数是thread_do,向函数传的是一个结构体的地址

    if(err != 0) {
        fprintf(stderr, "can't create thread: %s\n", strerror(err));//习惯性定义,在线程中使用fprintf
    }

    while (1) {
        printf("main thread pid = %x, child tid = %x\n", (int)pthread_self(), (int)tid);
        //打印本线程的id和子线程的tid
        sleep(1);
    }

    return 0;
}

当pthread_create里面的tid没有返回,而tread_do函数已经执行时,此刻,tread_do里的pthread_self得到的是线程id,而tid还没有被复制,里面是垃圾值, 例如把tid定义成全局变量,并且在线程函数调用的时候,可能会出问题。

pthread_exit

调用线程退出函数
如果使用exit时,任何线程里exit导致进程退出,其他线程工作没有结束就退出了,主控线程退出时不能return或exit,就不能关闭文件描述符,刷新缓冲区。
return 是只结束当前线程 
    #include <pthread.h>

    void pthread_exit(void *retval);
    void *retval:线程退出时传递出来的参数,可以是退出值或地址,如是地址时,不能是线程内部申请的局部地址。

_exit是exit的底层函数,使用_exit退出函数,不会刷新缓冲区,是和read、write一个层面的。

pthread_jojin

回收线程,当线程没有结束时,调用该函数的线程将挂起等待
    #include <pthread.h>
    int pthread_join(pthread_t thread, void **retval);

    pthread_t thread:回收线程的tid
    void **retval:接收退出线程传递出的返回值
    返回值:成功返回0,失败返回错误号
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方式终止,通过pthread_join得到的终止状态是不同的,总结如下:
如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED。
如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。
如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数

例子:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

struct STU {
    int id; 
    char name[20];
};

void *do_thread_1(void *arg)
{
    printf("I am %lx\n", pthread_self());
    sleep(3);
    pthread_exit((void *)5);  //返回5

}

void *do_thread_2(void *arg)
{
    struct STU *p = (struct STU*)arg;
    p->id = 20; 
    strcpy(p->name, "xiaoming");
    printf("I am %lx\n", pthread_self());
    sleep(3);
    pthread_exit((void*)p); //返回结构体地址
}

int main(void)
{
    pthread_t tid;
    int *res;
    struct STU s = {100, "zhangsan"};
    struct STU *p;

    pthread_create(&tid, NULL, do_thread_1, NULL);
    printf("wait for child thread_1\n");
    pthread_join(tid, (void**)&res);
    printf("child thread return %d\n", (int)res);

    pthread_create(&tid, NULL, do_thread_2, (void*)&s);
    printf("wait for child thread_2\n");
    pthread_join(tid, (void **)&p);//因为这个返回的地址是二级指针,所以用&p接收
    printf("child thread return %d, %s\n", p->id, p->name);

    return 0;
}

pthread_cancel

在进程内的某个线程可以干掉同进程里的另一个线程。
#include <pthread.h>
int pthread_cancel(pthread_t thread);
被取消的线程,退出值,第一在Linux的pthread库中常数PTHREAD_CANCELED的值是-1.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

void *thr_fn1(void *arg)
{
    printf("tread 1 returning\n");
    return (void*)1;
}

void *thr_fn2(void *arg)
{
    printf("thread 2 exiting\n");
    pthread_exit((void *)2);
}

void *thr_fn3(void *arg)
{
    while(1) {
        //printf("thread 3 writing\n");
        //sleep(1);
    }   
}

int main(void)
{
    pthread_t tid;
    void *tret;

    pthread_create(&tid, NULL, thr_fn1, NULL);
    pthread_join(tid, &tret);
    printf("tread 1 exit code %d\n", (int)tret);    //return

    pthread_create(&tid, NULL, thr_fn2, NULL);
    pthread_join(tid, &tret);
    printf("tread 2 exit code %d\n", (int)tret);    //pthread_exit

    pthread_create(&tid, NULL, thr_fn3, NULL);
    sleep(3);
    pthread_cancel(tid);
    pthread_join(tid, &tret);
    if(tret == PTHREAD_CANCELED)    //pthread_cancel
        printf("I am cancel\n");
    printf("tread 3 exit code %d\n", (int)tret);

    return 0;
}
  • 如果把thr_fn3函数的while里面的两句话全部注释掉,在运行,会发现线程thr_fn3不会挂掉,这是因为第三个线程没有进入内核进行操作,所以没有触发cancel,这里可以类比信号,此时cancel这个操作还没有响应到第三个线程上,只有第三个线程进入内核的时候才会响应这个时间,线程才会退出,printf->write->system_write->内核,像n++这种操作就没有进入内核
  • 如果线程内本就没有进入内核的操作,但还是需要使用pthread_cancel这个函数来干掉他,那就让线程调用pthread_testcancel(),让内核去检测是否需要取消当前线程

pthread_detach

分离线程
    #include <pthread.h>
    int pthread_detach(pthread_t tid);
    返回值:成功0, 失败返回错误号。
一般情况下,线程终止后,其终止状态一直保留到其他线程地调用pthread_join获取他的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它所占用的所有资源,而不保留终止状态,这种线程也叫游离线程。不能对一个已经处在detach状态的线程调用pthread_join,这样的调用将返回EINVAL。如果已经对一个线程调用了pthread_detach就不能在调用pthread_join了
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

void *thr_fn(void *arg)
{
    int n = 3;
    while (n--) {
        printf("thread count %d\n", n); 
        sleep(1);
    }   
    return (void *)1;
}


int main(void)
{
    pthread_t tid;
    void *tret;
    int err;

    pthread_create(&tid, NULL, thr_fn, NULL);
    //pthread_detach(tid); //加上这句和不加上这句看看有什么差别

    while(1) {
        err = pthread_join(tid, &tret);
        if(err != 0)
            fprintf(stderr, "thread %s\n", strerror(err));
        else
            fprintf(stderr, "thread exit code %d\n", (int)tret);
        sleep(1);
    }

    return 0;
}

pthread_equal

比较两个线程的tid是否相等
    #include <pthread.h>
    int phtread_equal(pthrad_t t1, pthread_t t2);

    返回值,相同返回一个非零值,不相等返回0

线程属性

默认属性已经可以解决绝大多是问题。如果我们队程序i的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程的栈大小来降低内存的使用,增加最大线程个数。

早期的线程属性定义,方便理解。

    typedef struct {
        int                 etachstate; //线程的分离状态
        int                 schedpolicy;    //线程调度策略
        structsched_param   schedparam; //线程调度参数
        int                 inheritsched;   //线程的继承性
        int                 scope;  //线程的作用域
        size_t              guardsize;  //线程栈末尾的警戒缓冲区大小
        int                 stachaddr_set;  //线程的栈设置
        void*               stachaddr;  //线程栈的位置
        size_t              stacksize;  //线程栈的大小
    }pthread_attr_t;

pthread_attr_init

初始化线程属性
int pthread_attr_init(pthread_attr_t *attr);    //初始化线程属性
int pthread_attr_destroy(pthread_attr_t *attr); //销毁线程属性所占的资源

线程的分离状态

    #include <pthread.h>
    int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); //设置线程属性,分离or非分离
    int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate); //获取线程属性,分离or非分离

pthread_attr_t *attr:被已经初始化的线程属性
int detachstate:可选为PTHREAD_CREATE_DETACHED(分离线程)和PTHREAD_CREATE_JOINABLE(非分离线程)
如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号,为了避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timewait函数,让这个线程等一会。

线程的栈地址和栈大小

#include <pthread.h>
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
int pthread_attr_getstack(pthread_attr_t *attr, void **stackaddr, size_t stacksize);

attr        指向一个线程属性的指针
stackaddr   返回获取的栈地址
stacksize   返回获取的栈大小
返回值,若成功返回0,否则返回错误的编号

例子

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <stdlib.h>

#define SIZE 0x10000


void *do_thread(void *arg)
{
    printf("hello\n");
    while (1) 
        sleep(1);
}

int main(void)
{
    pthread_t tid;
    int err, detachstate, i=1;
    pthread_attr_t attr;
    size_t stacksize;
    void *stackaddr;

    pthread_attr_init(&attr);   //初始化属性

    pthread_attr_getstack(&attr, &stackaddr, &stacksize);   //获取默认属性的栈地址,栈大小
    printf("stackadd = %p\n", stackaddr);
    printf("stacksize = %x\n", (int)stacksize);

    pthread_attr_getdetachstate(&attr, &detachstate);   //获取默认属性中的detach状态
    if (detachstate == PTHREAD_CREATE_DETACHED) 
        printf("thread detached\n");
    else if (detachstate == PTHREAD_CREATE_JOINABLE) {
        printf("thread join\n");
        pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);    //如果不是detach,就把他设置成detach
    } else
        printf("thread unknown\n");

    while(1) {
        stackaddr = calloc(SIZE, 1);    //在堆上申请内存
        if(stackaddr == NULL) {
            perror("calloc");
            exit(1);
        }

        stacksize = SIZE;   //堆内存的大小
        pthread_attr_setstack(&attr, stackaddr, stacksize); //把堆内存的地址和大小放到属性里面

        err = pthread_create(&tid, &attr, do_thread, NULL);//创建attr这种属性的线程
        if (err != 0) {
            printf("%s\n", strerror(err));
            exit(1);
        }
        printf("%d\n", i++);
    }

    pthread_attr_destroy(&attr);    //释放属性空间
    return 0;
}

注意

1.主线程退出其他线程不退出,主线程应调用pthread_exit
2.避免僵线程
3.malloc和mmap申请的内存可以被其他线程释放
4.如果线程终止时没有释放加锁的互斥量,则该互斥量不能再被使用
5.应避免在多线程模型中调用fork,除非马上exec,子进程中只有调用fork的线程存在,其他线程在子进程中均pthread_exit
6.信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制。

你可能感兴趣的:(线程,linux)