首页 > 代码库 > Unix线程概念、控制原语、属性
Unix线程概念、控制原语、属性
线程:
线程基础概念:
线程在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 <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
void *print_id(void *arg) //线程所执行的函数
{
printf("%dth pthread id:%lu\n", (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\n", strerror(ret)); //由于线程创建失败返回的错误码不保存在errno中,所以用strerror函数将其转成错误信息进行输出
}
}
printf("%dth pthread id:%lu\n", i, pthread_self());
sleep(5); //这里是为了防止各线程还没执行完,进程就先退出了
return 0;
}
需要记住,线程间共享全局变量
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
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\n", strerror(ret));
}
sleep(1);
printf("var : %d \n", var); //输出var的值,输出结果为200。已经改变。
sleep(1);
return 0;
}
线程退出:
函数原型:void pthread_exit(void *retval)
参数:retval:表示线程退出状态,通常传NULL
注意:exit()
函数的作用是退出当前进程,而pthread_exit()
函数是退出当前线程。也就是说,如果线程中调用了exit()
函数,那么这个进程就退出了,程序也就结束了。
这里我们借助之前循环创建线程的代码来进行一个测试,加强线程和进程之间关系的理解:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
void *print_id(void *arg)
{
sleep(2); //相比于之前,这里多加了睡眠2s
printf("%dth pthread id:%lu\n", (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\n", strerror(ret));
}
if(i == 2) //创建了3个线程之后,主线程就退出
pthread_exit(NULL);
}
printf("%dth pthread id:%lu\n", 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 <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
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\n", strerror(ret));
}
else //否则输出退出状态
{
printf("exit code : %d\n", 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 <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
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\n", (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 <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#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\n");
else if (detachstate == PTHREAD_CREATE_JOINABLE) //如果不是分离态
printf("thread join\n");
else
printf("thread unknown\n");
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\n", strerror(err));
exit(1);
}
printf("%d\n", i++);
}
pthread_attr_destroy(&attr);
return 0;
}
线程使用注意事项:
1.malloc和mmap申请的内存可以被其它线程释放(因为堆空间共享)
2.避免有僵尸线程,浪费资源
3.如果在多线程中调用fork并且不马上exec,那除了调用fork的线程存在,其它的线程全部都会pthread_exit。
Unix线程概念、控制原语、属性