线程的概念
1.与进程(process)类似,线程(thread)是允许应用程序并发执行多个任务的一种机制。一个进程可以包含多个线程。同一个程序中的所有线程均会独立执行相同的程序,并且共享同一份全局内存区域,其中包括初始化数据段(.data),未初始化数据段(.bss),栈内存段。
【注意:没有共享栈内存和代码段】
2.进程是CPU分配资源的最小单位,线程是操作系统调度执行的最小单位
3.线程是轻量级的进程(LWP:light weight process),在Linux环境下线程的本质仍然是进程
4.查看指定进程的LWP:ps -lf pid (注:lwp不是线程的id)
Linux内核线程实现原理
1.轻量级进程(light-weight process)也有PCB,创建线程使用的底层函数和进程一样,都是clone
2.从内核里看进程和线程是一样的,都有各自不同PCB,但是PCB中指向内存资源的三级页表是相同的
三级映射:进程PCB-->页目录(可以看出是数组,首地址位于PCB中)-->页表-->物理页面-->内存单元
3.进程可以蜕变成线程(看上面的虚拟地址空间就能分析出来)
4.线程可看作寄存器和栈的集合
5.LWP和进程ID的区别:LWP号是Linux内核划分时间轮片给线程的依据,线程ID是在进程内部区分线程的
a
6.对于进程而言,相同地址在不同的进程中,反复使用而不冲突。原因是他们虽然虚拟地址一样,但页目录、页表、物理页面各不相同。相同的虚拟地址,映射到不同的物理页面内存单元,最终访问不同物理页面。
但,线程不同。两个线程虽然有独立的PCB,但是共享同一个页目录、页表、物理页面。所以两个PCB共享同一个地址空间。
7.无论是创建进程的fork()还是创建线程的pthread_creat(),底层实现都是调用一个内核函数clone()。如果是复制对方的地址空间,那么就产生一个“进程”,如果是共享对方的地址空间,就产生一个"线程"。因此,Linux内核是不区分进程和线程的。只是在用户层面上进行区分。所以,线程所有的操作函数pthrad_*是库函数,而非系统调用。
进程和线程区别总结
1.进程间的信息难以共享。由于除去只读代码段外,父子进程并未共享内存,因此必须采用一些进程间通信方式,在进程间进行信息交换。
2.调用fork()来创建进程的代价相对较高(复制一份地址空间),即便采用"写时复制"机制,仍然需要复制诸如内存页表和文件描述符表之类的多种进程属性,这意味着fork()调用在时间上的开销依然不菲
3.线程之间能够方便、快速地共享信息,只需要将数据复制到共享(栈不行,见上图)变量中即可
4.创建线程比创建进程通常要快10倍甚至更多。线程间是共享虚拟地址空间的,无需采用写时复制来复制内存,也无需复制页表(之前说过虚拟地址空间的实现,并不是真的去分配4G的内存,只需要实现相应的数据结构,页表、页目录)
线程之间共享和非共享资源
这个图太重要了,我们再看一下
共享资源 | 非共享资源 |
文件描述符表 | 线程ID |
每种信号的处理方式 | 处理器现场和栈指针(内核栈) |
当前工作目录、文件权限 | 独立的栈空间(用户空间栈) |
用户ID和组ID和会话ID | errno变量 |
虚拟地址空间(除栈 .txt) | 信号屏蔽字 |
调度优先级 |
介绍下NPTL(第三方库):
当Linux最初开发时,在内核中并不能正真支持线程。通过clone()系统调用进程作为可调度的实体。这个调用创建了调用进程的一个拷贝,这个拷贝于调用共享相同的地址空间。但这个符合POSIX的要求,在信号处理、调度间同步等方面存在问题。
NPTL或称为Native POSIX Thread Library。是Linux线程的一个新实现,它克服了LinuxThreads的缺点,同时符合标准。
查看当前进程库版本:getconf GNU_LIBPTHREAD_VERSION
线程优缺点
优点:1.提高程序并发性 2.开销小 3.数据通信、共享数据方便
缺点:1.库函数、不稳定 3.调试、编写困难(gdb不支持) 4.对信号支持不好
优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大
线程相关函数(线程控制原语)
一般情况下,main函数所在的线程称为主线程。其余创建的线程称为子线程。
fork()函数--->创建进程
程序中默认只有一个线程,pthread_create()函数调用-->2个线程
pthread_create函数
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
失败:返回错误号
补充说明:因为线程是通过NPTL库是第三方库,跟我们的系统调用不一样哦。
之前我们一直使用perror(ret),现在可行不通. 使用 char *strerror(int errnum)
#include <iostream> #include <pthread.h> #include <string.h> #include <unistd.h> using namespace std; void *func(void *arg) { printf("pthread:%lu\n",pthread_self()); return NULL; } int main(void) { //typedef unsigned long pthread_t tid; int ret = pthread_create(&tid,NULL,func,NULL); if(ret) { printf("%s\n",strerror(ret)); exit(-1); } sleep(1); printf("我是主线程:%d\n",getpid()); return 0; }
编译注意事项:线程椒通过第三方库实现的
g++ createPthread.cpp -o createPthread -lpthread(-pthread)
pthread_exit函数
int pthread_exit(void *retval);
功能:终止一个线程,在哪个线程中调用就代表终止哪个线程
参数:
retval:需要传递一个指针,作为一个返回值,在pthread_join()中可以获取到。
线程退出的状态,通常传NULL
补充:当主线程退出时,不会影响其他正常运行的线程
在线程中禁止使用exit函数,会导致进程内所有线程全部退出
#include <stdio.h> #include <pthread.h> #include <string.h> #include <unistd.h> void *func(void *arg) { int i = (int)arg; if(i == 2) { pthread_exit(NULL); } sleep(2); printf("我是第%d号线程,线程ID:%lu\n",i,pthread_self()); return NULL; } int main(void) { pthread_t tid; for(int i=0;i<5;++i) { pthread_create(&tid,NULL,func,(void *)i); } sleep(5); printf("我是主线程,线程ID:%lu\n",pthread_self()); return 0; }
pthread_self函数
pthread_t pthread_self(void)
功能:获取当前的线程的线程ID
pthread_equal函数
int pthread_equal(pthread_t t1,pthread_t t2);
功能:比较两个线程ID是否相等
返回值:相等非0,不相等0
补充:不同操作系统pthread_t类型的实现不一样,有的时无符号长整型,有的可能是结构体
#include <stdio.h> #include <pthread.h> #include <unistd.h> pthread_t tid_one; void *func(void *arg) { if(pthread_equal(tid_one,pthread_self())) { printf("相等\n"); } else { printf("不相等\n"); } } int main(void) { pthread_t tid; pthread_create(&tid_one,NULL,func,NULL); pthread_join(tid_one,NULL); return 0; }
pthread_join函数
int pthread_join(pthread_t thread,void ** retval);
功能:和一个已经终止的线程进行连接,回收资源
回收子进程的资源
这个函数是阻塞的,调用一次只能回收一个子进程
一般在主线程中使用
参数:thread 需要回收的子进程ID
retval 接收子进程退出时的返回值
返回值:
0 成功
非0,失败,返回错误号
线程的分离
int pthread_detach(pthread_t thread);
功能:分离一个线程。被分离的线程在终止的时候,会自动释放资源返回给系统
注意事项:
1).不能多次分离,会产生不可预测的行为
2).不能去连接一个已经分离的线程,会报错【pthread_join】
参数:
需要分离的线程的ID
返回值:
成功0 失败错误号
重点说明:
线程分离状态:指定该状态,线程主动与主控制线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用。
进程如果有这样的机制,将不会产生僵尸进程,僵尸进程的产生主要由于进程死亡后,大部分资源被释放,一点残留资源仍然在系统中,导致内核认为该进程仍然存在
一般情况下,线程终止后,其终止状态一直保留到其他线程调用pthread_join获取它的状态为止,但是线程也可以被设置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。不能对一个已经处于detach状态的线程调用pthread_join这样的调用将返回EINVAL错误。
#include <stdio.h> #include <pthread.h> void* func(void *arg) { printf("我是子线程:%lu\n",pthread_self()); return NULL; } int main(void) { pthread_t tid; pthread_create(&tid,NULL,func,NULL); //设置线程分离 pthread_detach(tid); sleep(3); return 0; }
线程的取消
int pthread_cancel(pthread_t thread);
功能:取消线程(让线程终止)
取消某个进程,可以终止某个线程的运行。
但不是立马终止,而是当子线程执行到一个取消点,线程才会终止
取消点:系统调用(从用户态切换到内核态的时候) creat open pause read write..
如果线程终没有取消点,可以通过调用pthread_testcancel函数自行设置一个取消点
被取消的线程,退出值定义在Linux的pthread库中。常数PTHREAD_CANCELED的值是一个 -1。可在pthread.h中找到它的定义。
因此我们对一个已经被取消的线程使用pthread_join回收时,得到的返回值-1
#include <stdio.h> #include <pthread.h> void *func(void *arg) { //printf("我是子线程:%lu\n",pthread_self()); //printf会产生一个系统调用 pthread_testcancel(); //自己设置一个取消点 return NULL; } int main(void) { pthread_t tid; pthread_create(&tid,NULL,func,NULL); //取消这个线程 pthread_cancel(tid); //回收 int childRet; pthread_join(tid,(void **)&childRet); //阻塞的 printf("回收的返回值:%d\n",childRet); return 0; }