7.1线程2015/8/2

2019-07-14 11:04发布


线程概念

线程和进程的关系

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 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 #include #include #include #include #include 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 ", 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 ", strerror(err));//习惯性定义,在线程中使用fprintf } while (1) { printf("main thread pid = %x, child tid = %x ", (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 void pthread_exit(void *retval); void *retval:线程退出时传递出来的参数,可以是退出值或地址,如是地址时,不能是线程内部申请的局部地址。 _exit是exit的底层函数,使用_exit退出函数,不会刷新缓冲区,是和read、write一个层面的。

pthread_jojin

回收线程,当线程没有结束时,调用该函数的线程将挂起等待 #include 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 #include #include #include #include struct STU { int id; char name[20]; }; void *do_thread_1(void *arg) { printf("I am %lx ", 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 ", 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 "); pthread_join(tid, (void**)&res); printf("child thread return %d ", (int)res); pthread_create(&tid, NULL, do_thread_2, (void*)&s); printf("wait for child thread_2 "); pthread_join(tid, (void **)&p);//因为这个返回的地址是二级指针,所以用&p接收 printf("child thread return %d, %s ", p->id, p->name); return 0; }

pthread_cancel

在进程内的某个线程可以干掉同进程里的另一个线程。 #include int pthread_cancel(pthread_t thread); 被取消的线程,退出值,第一在Linux的pthread库中常数PTHREAD_CANCELED的值是-1. #include #include #include #include void *thr_fn1(void *arg) { printf("tread 1 returning "); return (void*)1; } void *thr_fn2(void *arg) { printf("thread 2 exiting "); pthread_exit((void *)2); } void *thr_fn3(void *arg) { while(1) { //printf("thread 3 writing "); //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 ", (int)tret); //return pthread_create(&tid, NULL, thr_fn2, NULL); pthread_join(tid, &tret); printf("tread 2 exit code %d ", (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 "); printf("tread 3 exit code %d ", (int)tret); return 0; }
  • 如果把thr_fn3函数的while里面的两句话全部注释掉,在运行,会发现线程thr_fn3不会挂掉,这是因为第三个线程没有进入内核进行操作,所以没有触发cancel,这里可以类比信号,此时cancel这个操作还没有响应到第三个线程上,只有第三个线程进入内核的时候才会响应这个时间,线程才会退出,printf->write->system_write->内核,像n++这种操作就没有进入内核
  • 如果线程内本就没有进入内核的操作,但还是需要使用pthread_cancel这个函数来干掉他,那就让线程调用pthread_testcancel(),让内核去检测是否需要取消当前线程

pthread_detach

分离线程 #include int pthread_detach(pthread_t tid); 返回值:成功0, 失败返回错误号。 一般情况下,线程终止后,其终止状态一直保留到其他线程地调用pthread_join获取他的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它所占用的所有资源,而不保留终止状态,这种线程也叫游离线程。不能对一个已经处在detach状态的线程调用pthread_join,这样的调用将返回EINVAL。如果已经对一个线程调用了pthread_detach就不能在调用pthread_join了 #include #include #include #include #include void *thr_fn(void *arg) { int n = 3; while (n--) { printf("thread count %d ", 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 ", strerror(err)); else fprintf(stderr, "thread exit code %d ", (int)tret); sleep(1); } return 0; }

pthread_equal

比较两个线程的tid是否相等 #include 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 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 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 #include #include #include #define SIZE 0x10000 void *do_thread(void *arg) { printf("hello "); 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 ", stackaddr); printf("stacksize = %x ", (int)stacksize); pthread_attr_getdetachstate(&attr, &detachstate); //获取默认属性中的detach状态 if (detachstate == PTHREAD_CREATE_DETACHED) printf("thread detached "); else if (detachstate == PTHREAD_CREATE_JOINABLE) { printf("thread join "); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); //如果不是detach,就把他设置成detach } else printf("thread unknown "); 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 ", strerror(err)); exit(1); } printf("%d ", i++); } pthread_attr_destroy(&attr); //释放属性空间 return 0; }

注意

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