Unix线程概念、控制原语、属性

2019-07-14 09:23发布

线程:

线程基础概念:

线程在Linux中又称轻量级进程。而且它和进程都有PCB(进程控制块)。可是差别是进程的虚拟地址空间是独享的,也就是每一个进程都有自己的虚拟地址空间。可是线程的PCB是共享的,在同一个虚拟地址空间里面,每一个线程有自己的PCB。尽管每一个线程都有自己的PCB,可是从内核的角度来看,进程和线程是一样的,这是由于同一个虚拟地址空间里面的每一个线程的PCB指向的内存资源的三级页表是同样的。在Linux下,能够把线程看做是最小的运行单位(进程内部运用多线程完毕任务)。而进程是最小的分配资源单位(系统以进程为单位来创建,而没有创建一个线程来运行的说法)。实际上,不管是创建进程的fork,还是创建线程的pthread_create,底层的实现都是调用同一个内核函数clone。假设复制对方的地址空间。那就会产生一个”进程”。假设共享对方的地址空间,就产生一个”线程”。进程能够看作是仅仅有一个线程的进程。
由于。linux内核不区分进程和线程,仅仅在用户层面上区分。所以全部有关线程操作的函数都是库函数,而不是系统调用。

线程共享资源:

1.共享文件描写叙述符表(pcb都是指向的同一块物理地址,而文件描写叙述符表存在于pcb中,当然同样)
2.共享信号的处理方式(同理)
3.共享当前工作文件夹(同理)
4.共享进程ID和组ID(线程还是处于进程中的。所以进程ID和组ID都同样)
5.共享一部分内存地址空间(.text/.data/.bss/heap/共享库)(方便了数据共享和同步)

线程非共享资源:

1.线程id(在同一个进程中,为了标识不同的线程)
2.寄存器的值(由于线程是并发运行的,每一个线程有自己不同的运行情况,线程间进行切换时,必须要将原来的线程的寄存器集合的值保存下来。以方便又一次切换回来的时候恢复)
3.栈空间(栈空间的独立保证了线程独立运行。不受其他线程的影响)
4.errno变量(同样也是保障线程的独立运行。不受其他线程的影响)
5.信号屏蔽字(同理)
6.调度优先级(线程须要像进程那样被调度,所以须要有被调度的參数,就是优先级)

线程优缺点:

长处:
1.提高了程序的并发性
2.开销比进程小(不用像进程那样,每次都创建自己独有的虚拟空间)
3.数据通信和共享方便(由于线程共享了一部分内存地址空间)
缺点:
1.库函数不如系统调用稳定
2.gdb不支持其调试(gdb的产生远早于线程的增加)
3.对信号的支持不好(同样信号的诞生和线程并非同一时期)

控制原语:

查看线程ID:
函数原型:pthread_t pthread_self(void)
返回值:返回线程ID,无失败情况(由于就算不创建线程,进程能够看作是仅仅有一个主线程的进程)。pthread_t类型在linux系统下是无符号整数。在其他系统可能是结构体。而且线程ID是进程内部的识别标志。所以不同进程间线程ID同意同样。
创建线程:
函数原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
返回值:成功返回0.失败返回错误号
參数:thread:传出參数。保存系统为我们分配好的线程ID。attr:通常传NULL,表示使用线程默认属性。;start_routine:函数指针,指向线程的主函数,该函数运行结束之后,该线程也结束。arg:线程主函数运行时传入的參数
注意:当调用了pthread_create函数之后,当前的线程会继续向下运行,而新创建的线程会去运行我们传入的start_routine函数,该函数运行结束后。新创建的这个线程也就结束了。 当我们使用gcc编译的关于线程操作的时候。须要额外加上-pthread參数。
样例: #include #include #include #include #include void *print_id(void *arg) //线程所运行的函数 { printf("%dth pthread id:%lu ", (int )arg, pthread_self()); } int main() { int ret; pthread_t tid; int i = 0; for(; i < 5; i++) //循环创建5个线程 { ret = pthread_create(&tid, NULL, print_id, (void *)i); if(ret != 0) { printf("%s ", strerror(ret)); //由于线程创建失败返回的错误码不保存在errno中,所以用strerror函数将其转成错误信息进行输出 } } printf("%dth pthread id:%lu ", i, pthread_self()); sleep(5); //这里是为了防止各线程还没运行完,进程就先退出了 return 0; } 须要记住,线程间共享全局变量 #include #include #include #include int var = 100; void *glb_share(void *arg) //改变全局变量var的值 { var = 200; } int main() { pthread_t tid; int ret; if((ret = pthread_create(&tid, NULL, glb_share, NULL)) != 0) { printf("%s ", strerror(ret)); } sleep(1); printf("var : %d ", var); //输出var的值。输出结果为200。已经改变。 sleep(1); return 0; } 线程退出:
函数原型:void pthread_exit(void *retval)
參数:retval:表示线程退出状态。通常传NULL
注意:exit()函数的作用是退出当前进程,而pthread_exit()函数是退出当前线程。也就是说,假设线程中调用了exit()函数,那么这个进程就退出了,程序也就结束了。
这里我们借助之前循环创建线程的代码来进行一个測试,加强线程和进程之间关系的理解: #include #include #include #include #include void *print_id(void *arg) { sleep(2); //相比于之前,这里多加了睡眠2s printf("%dth pthread id:%lu ", (int )arg, pthread_self()); } int main() { int ret; pthread_t tid; int i = 0; for(; i < 5; i++) { ret = pthread_create(&tid, NULL, print_id, (void *)i); if(ret != 0) { printf("%s ", strerror(ret)); } if(i == 2) //创建了3个线程之后,主线程就退出 pthread_exit(NULL); } printf("%dth pthread id:%lu ", i, pthread_self()); sleep(5); return 0; } 运行的结果是会输出创建的前3个线程的ID的,在线程sleep的那2秒中,主线程已经退出了。可是其余的线程还能够继续运行。这就说明了线程之间的独立性,而主函数之后的代码也不会运行了,所以印证了之前说的能够把一个进程看成是拥有一个主线程的进程这句话。假如将pthread_exit(NULL)函数换成exit(1)。能够发现程序立即就结束了。这说明了exit函数是退出进程。而pthread_exit是退出单个线程。 堵塞等待线程退出:
函数原型:int pthread_join(pthread_t thread, void **retval)
返回值:成功返回0。失败返回错误号
參数:thread:要退出的线程的ID;retval:存储线程结束状态,假设是被其他线程调用pthread_cancel异常终止了,retval存放的值是常量PTHREAD_CANCELED。 线程分离:
函数原型:int pthread_detach(pthread_t thread)
返回值:成功返回0。失败返回错误号
參数:thread:要进行分离的线程ID
线程分离状态:线程主动与主线程断开联系。线程结束后,其退出状态不被其他线程获取。而直接自己自己主动释放。在普通情况下,线程终止之后,它的终止状态一直保留到其他线程调用pthread_join获取为止,然而设置为分离态之后,线程一旦终止就立马回收它占用的全部资源,不会保留终止状态。所以不能对一个设置了分离态的线程调用pthread_join。
样例: #include #include #include #include #include void *detach_pthread(void *arg) { int exit_code = 233; pthread_exit((void *)exit_code); } int main() { pthread_t tid; int ret; void *retval; pthread_create(&tid, NULL, detach_pthread, NULL); pthread_detach(tid); //设置分离态 ret = pthread_join(tid, &retval); //堵塞回收线程,并接收返回状态 if(ret != 0) //pthread_join调用失败之后输出原因 { printf("pthread_join error:%s ", strerror(ret)); } else //否则输出退出状态 { printf("exit code : %d ", retval); } return 0; } 这段代码运行的结果是:pthread_join error:Invalid argument(无效的參数)
这就说明了设置了分离态的线程已经脱离了pthread_join回收的范围了。
另外也能够通过设置线程的属性来达到线程分离。 杀死线程:
函数原型:int pthread_cancel(pthread_t thread)
返回值:成功返回0,失败返回错误码
參数:thread:要杀死的线程号
注意:这个函数并不像kill函数调用了就杀死所指定,而是要到达一个取消点(检查线程是否被取消)。一般是一些系统调用有取消点。比方read write close等。只是我们能够调用pthread_testcancel()函数充当一个取消点,被取消的线程的返回值是PTHREAD_CANCELED(-1)。 样例: #include #include #include #include void *test_cancel(void *arg) { int val = 3; while(1) { pthread_testcancel(); } pthread_exit((void *)val); } int main() { pthread_t tid; void *ret = NULL; pthread_create(&tid, NULL, test_cancel, NULL); pthread_cancel(tid); //杀死创建的进程 pthread_join(tid, &ret); //堵塞等待线程退出。并获取退出状态 printf("thread exit code = %d ", (int)ret); return 0; } 这段代码输出的结果是thread exit code = -1,说明了线程成功被杀死了,不然会返回3。 检查两个线程ID是否同样:
函数原型:int pthread_equal(pthread_t t1, pthread_t t2);
返回值:假设线程ID同样,返回非0值,否则返回0。没有失败情况。

线程属性:

默认属性能够解决大多数情况。可是假设对性能有更高的要求,就能够通过改动线程属性。降低线程栈的大小,来降低内存的使用。
设置线程属性的结构体: typedef struct { int detachstate; //线程分离状态 int schedpolicy; //线程调度策略 struct sched_param schedparam; //线程的调度參数 int inheritsched; //线程的继承性 int scope; //线程的作用域 size_t guardsize; //线程栈末尾的警戒缓冲区大小 int stackaddr_set; //线程的栈设置 void* stackaddr; //线程栈的位置 size_t stacksize; //线程栈的大小 }pthread_attr_t; 主要结构体成员:
1.线程分离状态
2.线程栈大小(默认平均分配)
3.线程栈警备缓冲区大小(位于栈末尾)

属性值不能直接设置。须要用对应的函数操作。
部分相关函数:
线程属性初始化:
函数原型:int pthread_attr_init(pthread_attr_t *attr)
返回值:成功返回0,失败返回错误号
參数:attr:设置属性的结构体
注意:须要先初始化属性,然后设置相关属性,再创建线程。
销毁线程属性:
函数原型:int pthread_attr_destroy(pthread_attr_t *attr)
返回值:成功返回0,失败返回错误号
參数:要销毁的线程属性
获取线程分离状态:
函数原型:int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate)
返回值:成功返回0,失败返回错误码
參数:attr:设置属性的结构体;detachstate:传入參数,获取状态,PTHREAD_CREATE_DETACHED(分离态)PTHREAD_CREATE_JOINABLE(非分离态)。
设置线程分离状态:
函数原型:int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
返回值:成功返回0.失败返回错误码
參数:attr:设置属性的结构体;detachstate:传出參数。设置状态,PTHREAD_CREATE_DETACHED(分离态)PTHREAD_CREATE_JOINABLE(非分离态)。
获取线程的栈大小:
函数原型:int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);
返回值:成功返回0。失败返回错误码
參数:attr:设置属性的结构体;stacksize:默认的栈的大小
设置线程的栈大小:
函数原型:int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
返回值:成功返回0,失败返回错误码
參数:attr:设置属性的结构体;stacksize:传入參数。将设置的栈的大小 获取线程的栈的首地址和大小:
函数原型:int pthread_attr_getstack(pthread_attr_t *attr, void **stackaddr, size_t *stacksize);
返回值:成功返回0,失败返回错误码
參数:attr:设置属性的结构体;stackaddr:传出參数,栈的首地址;stacksize:传出參数,栈的大小
设置线程的栈的首地址和大小:
函数原型:int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
返回值:成功返回0。失败返回错误码
參数:attr:设置属性的结构体;stackaddr:传入參数,新的栈的首地址。stacksize:传入參数,设置的栈的大小
当剩下的栈空间不够的时候,我们能够通过malloc函数或者mmap分配的空间作为新的栈的空间。 样例: #include #include #include #include #define SIZE 0x100000 void *th_fun(void *arg) { 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); //获取栈的信息 pthread_attr_getdetachstate(&attr, &detachstate); //获取分离态信息 if (detachstate == PTHREAD_CREATE_DETACHED) //假设是分离态 printf("thread detached "); else if (detachstate == PTHREAD_CREATE_JOINABLE) //假设不是分离态 printf("thread join "); else printf("thread unknown "); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); //将线程设置为分离态 while (1) { stackaddr = malloc(SIZE); //申请内存 if (stackaddr == NULL) { perror("malloc"); exit(1); } stacksize = SIZE; pthread_attr_setstack(&attr, stackaddr, stacksize); //设置栈大小 err = pthread_create(&tid, &attr, th_fun, NULL); if (err != 0) { printf("%s ", strerror(err)); exit(1); } printf("%d ", i++); } pthread_attr_destroy(&attr); return 0; } 线程使用注意事项:
1.malloc和mmap申请的内存能够被其他线程释放(由于堆空间共享)
2.避免有僵尸线程,浪费资源
3.假设在多线程中调用fork而且不立即exec,那除了调用fork的线程存在。其他的线程全部都会pthread_exit。