重谈地址空间–页表
线程是进程内的一个执行分支,一个进程内有多行代码,线程通常情况下只执行这多行代码的部分代码。更准确的定义是:线程是“一个进程内部的控制序列”。
一个进程内至少有一个执行线程
线程在进程内部运行,本质是在进程地址空间内运行
在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
操作系统中存在大量的进程,一个进程中又存在一个或多个线程,因此线程的数量一定比进程的数量多,很明显线程的执行粒度要比进程更细。
若一款操作系统要真正意义上支持线程,那么就需要对线程进行管理。比如创建线程、终止线程、调度线程、切换线程、给线程分配资源、释放资源以及回收资源等等,所有的这一套相比较进程都需要另起炉灶,搭建一套线程管理模块。
因此,若要支持真的线程一定会提高设计操作系统的复杂程度。在Linux看来,描述线程的控制块和描述进程的控制块是类似的,因此Linux并没有重新为线程设计管理模块,而是直接复用了进程控制块,即Linux中的所有执行流都是轻量级进程
但也有支持真正线程的操作系统,譬如Windows操作系统就存在专门描述线程的控制块,因此Windows操作系统系统的实现逻辑一定比Linux操作系统更为复杂
概念说明:
计算密集型(CPU密集型):执行流的大部分任务,主要以计算为主。如加密解密、大数据查找等
IO密集型:执行流的大部分任务,主要以IO为主。如刷盘、访问数据库、访问网络等
线程共享进程数据,但是也拥有自己的一部分数据:
栈、线程ID、一组寄存器(用来恢复上下文)、errno、信号屏蔽字、调度优先级
进程的多个线程共享同一块地址空间,如果定义一个函数、全局变量各个线程都可以访问,各线程还共享以下资源:
文件描述符、每一个信号的处理方式、当前工作目录、用户id和组id
在Linux中,站在内核角度上看并没有真正意义上线程相关的接口。但站在用户角度,当用户想创建一个线程时更期望使用thread_create这样类似的接口,而不是vfork函数,因此系统在应用层提供了原生线程库pthread。原生线程库实际就是对轻量级进程的系统调用进行了封装,在用户层模拟实现了一套线程相关的接口
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
参数:
线程创建成功返回0,失败返回错误码
使用案例
#include<iostream>
using namespace std;
#include<pthread.h>
#include<unistd.h>
void* Rountine(void* args)
{
while(1)
{
cout<<"i am"<<(char*)args<<endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,Rountine,(void*)"thread one");
while(1)
{
cout<<"i am main thread"<<endl;
sleep(1);
}
return 0;
}
使用 ps -aL 命令,可以显示当前的轻量级进程,不带 -L 选项默认显示进程
LWP(Light Weight Process)就是轻量级进程的ID,可以看到显示的两个轻量级进程的PID是相同的,因为它们属于同一个进程
线程如同进程一般,也是需要被等待的。若主线程不对新线程进行等待,那么新线程的资源不会被回收,会发生类似于"僵尸进程"的问题,即内存泄漏。
使用pthread_join()可以进行线程等待
int pthread_join(pthread_t thread, void **retval);
参数:
返回值:
调用该函数的线程将阻塞到ID为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的
#include<iostream>
using namespace std;
#include<pthread.h>
#include<unistd.h>
void* Rountine(void* args)
{
int cnt=10;
while(1)
{
cnt--;
if(cnt==0) break;
cout<<"i am"<< (char*)args<<endl;
sleep(1);
}
return (void*)13;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,Rountine,(void*)"thread one");
// while(1)
// {
// cout<<"i am main thread"<<endl;
// sleep(1);
// }
void* ret=nullptr;
int n=pthread_join(tid,&ret);
if(n==0)
{
cout<<"线程等待成功"<<"返回结果为"<<(long long)ret<<endl;
}
else cout<<"等待失败"<<endl;
return 0;
}
这里我一开始是比较疑惑为什么是void类型。其实是这样想要得到输出参数的参数类型可能为int、double、自定义类型,所以用的是void类型。
在创建线程时指定的例程中使用return代表当前线程退出,但在main函数中使用return代表整个进程退出,即主线程退出了那么整个进程就退出了。
void pthread_exit(void *retval);
参数retval:线程退出时的退出信息
注意:
int pthread_cancel(pthread_t thread);
参数thread:被取消线程的标识符
返回值:线程取消成功返回0,失败返回错误码
线程是可以取消自己的(使用pthread_self()函数)。也可以让新线程取消主线程,但不建议这么使用,一般都是使用主线程去控制新线程的。
取消成功的线程的退出码一般是宏PTHREAD_CANCELED,即(void*)-1)
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成内存泄漏。但若本身并不关心线程的返回值,那么join也是一种负担,此时可将该线程进行分离,后续当线程退出时就会自动释放线程资源
线程若被分离了,这个线程依旧使用该进程的资源,且依旧在该进程内运行,甚至这个线程崩溃了一定会影响整个进程,只不过这个线程退出时不再需要主线程去join了,当这个线程退出时系统会自动回收该线程所对应的资源
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离
joinable和分离是冲突的,一个线程不能既是joinable又是分离的
使用pthread_detach()函数进程分离线程
int pthread_detach(pthread_t thread);
参数thread:被分离线程的标识符
返回值:线程分离成功返回0,失败返回错误码
线程库NPTL提供的pthread_self()函数,获取的线程标识符和pthread_create()函数第一个参数获取的线程标识符是一样的
线程ID到底是什么?可以将线程ID打印出来看看
#include<iostream>
using namespace std;
#include<pthread.h>
#include<unistd.h>
#include<string>
string ToHex(pthread_t tid)
{
char buf[1024];
snprintf(buf,sizeof(buf),"%p",tid);
return buf;
}
void* Rountine(void* args)
{
int cnt=10;
while(1)
{
cout<<(char*)args<<":"<<ToHex(pthread_self())<<endl;
sleep(1);
}
return (void*)13;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,Rountine,(void*)"thread one");
while(1)
{
cout<<"main thread"<<":"<<ToHex(pthread_self())<<endl;
sleep(2);
}
void* ret=nullptr;
int n=pthread_join(tid,&ret);
if(n==0)
{
cout<<"线程等待成功"<<"返回结果为"<<(long long)ret<<endl;
}
else cout<<"等待失败"<<endl;
return 0;
}
之前提到每个线程都有独占的栈,其中主线程采用的栈是进程地址空间中原生的栈,而其余线程采用的栈就是在共享区中开辟的。除此之外,每个线程都有各自的struct pthread,当中包含了对应线程的各种属性;每个线程还有自己的线程局部存储,当中包含了对应线程被切换时的上下文数据。
每一个新线程在共享区都有一个struct pthread对其进行描述,因此要找到一个用户级线程只需要找到该线程内存块的起始地址,然后就可以获取到该线程的各种信息
上面讲述的各种线程函数,本质上都是在库内部对线程属性进行的各种操作,即线程数据的管理本质是在共享区的进行的
至于pthread_t到底是什么类型取决于实现,但对于Linux目前实现的NPTL线程库来说,线程标识符本质就是进程地址空间共享区上的一个虚拟地址,同一个进程中所有的虚拟地址都是不同的,因此可以用它来唯一区分每一个线程