线程概念、线程创建、线程退出及线程等待

2019-07-14 08:52发布

对于线程有一下学习结构:
这里写图片描述
cpu资源足够的情况下:为什么三个进程可以同时运行?
进程调度:cpu调度进程时,调度的是PCB
因为三个进程有三个PCB,因此在cpu足够的情况下,三个进程可以实现并行。
1.什么是线程?
Linux下pcb是线程:
  • Linux操作系统下,pcb是线程,其他操作系统不是这样;
  • 一个进程中至少有一个线程,线程是运行在进程内部;
  • Linux对进程和线程不作区分,linux下的线程以进程pcb模拟,linux下的进程控制块pcb实际是线程。
  • Linux下的线程以进程pcb模拟的,因此也叫轻量级进程,一个进程里的线程共享进程的虚拟地址空间。
    进程:
    以前的进程是具有一个线程的线程组;
    现在的进程是具有一个或多个线程的线程组。
    进程是操作系统资源分配的一个基本单位;
    线程是cpu调度(调度pcb)的一个基本单位。

    线程共享进程数据,但也拥有自己的一部分数据:线程ID、一组寄存器(上下文数据,用于cpu调度切换、栈、errno、信号屏蔽字、调度优先级)
    一个进程的多个线程共享:
    1. 同一地址空间,因此数据段,代码段都是共享的,如果定义一个函数,在各个线程都可以访问,如果定义一个全局变量,在各个线程都可以访问。
    2.文件描述符表
    3.每种信号处理方式(SIG_IGN即忽略信号、SIG_DFL即默认信号原本处理方式或者信号自定义的信号处理函数)
    4.当前工作目录
    5.用户id和组id
    每一个线程都需要有自己的栈区,否则如果线程共用一个栈,会引起调用栈混乱,并且因为cpu是以pcb来调度的,因为线程是cpu调度 的基本单位,所以每个线程都应该有自己的上下文数据来保存cpu调度切换时的数据(而这些都是在线程地址空间中,每一个线程都有自己的线程地址空间,它们相对独立,因为线程地址空间是在虚拟地址空间内 )
    线程和进程的优缺点:
    线程优点:
    一个进程中有可能会有多个线程,而这些线程共享同一个虚拟地址空间。
    1.因此有时也说线程是运行在进程中;
    2.因此线程间通信将变得很简单,因为共享同一个虚拟地址空间,即共享整个代码段;
    3.创建/销毁一个线程相较于进程来说成本更低(不需要额外创建虚拟地址空间…)
    4.线程的调度切换相较于进程 也较低
    线程缺点:
    一个进程中有可能会有多个线程,而这些线程共享同一个虚拟地址空间
    1.因为线程间的数据访问变得更加简单,因此数据安全访问问题更加突出,要考虑的问题增多,编码难度增加;
    2.一些系统调用和异常都是针对整个进程,因此当一个线程出现了异常,那么整个进程都会崩溃;系统调用也要注意。
    进程优点:安全、稳定(因为进程独立性)
    线程和进程哪个好?通过线程进程优缺点解答。
    2.线程的创建
    1.操作系统并没有提供系统调用来创建线程,所以大拿们就在posix标准库中,实现了一套线程控制(创建、终止、等待…)的接口
    2.因为这套接口创建的线程是库函数,是用户态的线程创建,因此创建的线程也叫用户线程
  1. 一个用户线程对应了一个轻量级进程来进行调度运行
    创建用户线程
pthread_create(创建用户线程) #include int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); thread:用于接受一个用户线程ID; attr : 用于设置线程属性,一般置NULL start_routine:是个函数地址,线程的入口函数,线程运行的就是这个函数,这个函数退出了,线程也就退出了 arg :用于给线程入口函数传递参数 返回值: 成功: 0 失败:errno number(错误码) 在task_struct有一个成员叫pid即进程id,但是现在看进程的话,一个进程中有可能会有多个线程,也就是会有多个pid;
因为现在的进程是线程组,其实在每个task_struct 都有一个标识说它们是哪个线程组的线程,这个成员叫tgid即线程组id或者用户态pid,然而这个进程组id(tgid)其实等于进程中主线程(首线程)task_struct中的pid,getpid()获取的是线程组id,而用户态ps -ef显示的进程信息其实是首线程信息,那么如何查看进程内所有线程信息?
ps -efL
LWP :轻量级进程,这一列记录的就是每个task_struct 中的pid
NLWP:这个进程中有几个线程
PID: 用户态展现出来的PID这一列是其实是tgid(线程组)值
这里写图片描述
NLWP :2,代表有两个线程;
进程ID是2516,两个线程ID分别为2516,2517,线程组内的第一个线程在用户态被称为主线程即线程组ID(2516)。 #include #include #include #include #include void *thr_start(void *arg) { int num=(int)arg; while(1) { printf("hello one "); sleep(1); } return NULL; } int main() { pthread_t tid; int ret; // int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); ret =pthread_create(&tid,NULL,thr_start,(void *)999); if(ret!=0)//成功返回0,失败返回错误码 { perror("pthread_create error "); return -1; } printf("child id:%p ",tid); printf("main id:%p ",pthread_self());//获取调用线程的线程id(用户态线程id) while(1){ printf("hello,another "); sleep(1); } return 0; } 这里写图片描述
运行结果:
这里写图片描述
pthread_create接口创建了一个用户线程,并且通过第一个参数返回来一个用户线程id,这个id数字十分大,其实这个id是一个地址,是一个指向线程自己的线程地址空间在整个进程虚拟地址空间中的位置。线程共享进程地址空间,但是每个线程都有自己相对独立的一个线程地址空间。相对独立是因为线程地址空间是在进程的虚拟地址空间之内的一段地址。
线程没有父子之分,所有的线程都是平级的,如果非要说有区别那么就是主线程与其他线程之分。
线程退出:

//这段代码用于演示线程退出的几种方式 #include #include #include #include #include #include void *thr_start(void *arg) { //exit(0); //return NULL; while(1) { printf("child thread "); sleep(1); } } int main() { pthread_t tid; int ret=-1; ret=pthread_create(&tid,NULL,thr_start,NULL); if(ret!=0) { perror("pthread_create error"); return -1; } // void pthread_exit(void *retval); // 参数retval 这个空类型指针保存的是线程退出后返回值 // pthread_exit或return返回的指针所指向的内存单元必须是全局的或者 //malloc分配的,不能在线程函数>的栈上分配,因为当其他线程得到这个返 //回指针时线程函数已经退出了。 //char *msg=malloc(10); //pthread_exit((void*)msg); // int pthread_cancel(pthread_t thread); //int ret1=-1; //ret1=pthread_cancel(tid); //printf("ret1=%d ",ret1); while(1) { printf("main thread "); sleep(1); } return 0; } 这里写图片描述
pthread_exit:
这里写图片描述
pthread_cancel:
#include int pthread_cancel(pthread_t thread); Compile and link with -pthread. 功能:取消线程id为thread的线程 返回值: 成功 0 ;失败 错误码 如果调用pthread_cancel,将取消普通线程,结果如下:
这里写图片描述
线程等待:
为什么要线程等待?线程等待是为了什么?
线程退出实际上也会形成僵尸线程占用了一部分资源而不释放,最终导致资源泄漏。线程等待就是接收线程的返回值,然后释放线程的所有资源。
pthread_join:
#include int pthread_join(pthread_t thread, void **retval); Compile and link with -pthread. 线程有一个属性:joinable 如果线程的属性是处于joinable状态,代表这个线程退出后需要被人等待, 一个线程只有处于joinable状态,才能被等待,而一个线程被创建出来,默认属性为joinable状态,而这个等待就是获取线程的返回值,并释放线程资源。 pthread_join: 功能:等待指定的线程并获取返回值 thread :用于指定等待哪个线程 retval: 用于接收线程的返回值 返回值:成功:0 失败 :错误码 如果调用join函数的时候线程已经退出了,那么将直接返回,否则将陷入阻塞等待,直到等待的线程退出。 如果一个线程是被取消的,那么它的返回值是-1。 pthread_detach
#include int pthread_detach(pthread_t thread); Compile and link with -pthread. 功能:设置detach状态,退出后自动释放资源 线程分离:我们等待一个进程是因为需要获取线程的返回值,并释放资源,那么假如我们根本不关心返回值,那么这个等待将毫无意义仅仅为了为了释放资源。
因此就有了一个线程属性:线程分离属性 detach属性,这个属性需要设置,它是告诉操作系统,这个指定的线程我不关心返回值,所以如果退出了就自动把所有资源回收。
而设置一个 线程分离属性称为分离线程;
线程的detach属性与joinable属性相对应,也相冲突,两者不会同时存在。如果一个线程属性是detach,那么调用pthread_join的时候将直接报错,所以只有一个线程处于joinable状态才可以被等待。
总结:线程如果被设置为detach属性,退出后将自动释放资源,不会形成僵尸进程,detach与joinable属性冲突,无法同时存在,设置detach就表明了不关心返回值,因此不能使用pthread_join等待线程获取返回值。