Linux中的线程
0.页表的正确理解
进程地址空间是进程能够使用的资源窗口,而页表则决定了进程实际上拥有的资源。
页表实际上是二级索引,以32位机器为例。内存管理的基本单位是4kb的页框。
一级页表是以地址的高10位为索引,二级页表是以次高10位为2级页表,找到页框的起始地址,最后再以低12位为偏移量找到实际上的物理地址
1. 线程的概念
进程是承担资源分配的基本实体,线程则是在进程内部的执行单元,是CPU调度的基本单位。
在Linux中并没有真正意义上的线程,有的只是轻量级进程。当我们创建线程时,实际上是在进程内部再创建一个轻量级进程,它与父进程指向同一个mem_struct,使用同一个页表,属于进程的一部分。
轻量级进程的实现实际上是用进程PCB模拟而来的,因此其具有与PCB相同的被执行,调度的方法。维护成本更低,更加的高效。
Linux只为我们提供了轻量级进程创建的接口,因此在此基础上为我们提供了一套原生线程库,实现了对底层接口的封装。
2.线程资源私有情况
线程之间绝大部分资源共享,但线程之间又有直接的私有空间
1️⃣线程pcb属性信息
2️⃣线程的上下文信息
3️⃣线程的独立栈结构
3.线程切换高效
CPU在处理线程切换时的工作要比进程切换的工作少。
进程切换时需要:切换进程地址空间,切换页表,切换PCB,切换上下文
线程切换时需要:切换PCB,切换上下文
但是最重要的是,进程切换时CPU中的Cache数据需要全部更新,而线程切换时CPU中的Cache数据并不用全部更新。因此线程切换要比进程切换更高效。
4.线程接口
4.1线程创建pthread_create
4.2线程终止
1️⃣当线程任务结束return后就会终止
2️⃣线程中调用pthread_exit(return val)时也会终止线程
3️⃣线程取消pthread_cancel(tid)时就会终止指定线程此时线程的返回值是(void*)-1
4.3线程等待pthread_join(pthread_t , void **)
与进程等待相同,线程也需要等待,目的是:获取返回值和回收线程的PCB控制块。
4.4线程分离pthread_detach(pthread_t tid)
如果我们不关心线程的返回值,想像进程对SIGCHILD采取SIG_IGN,让子进程自己回收释放时,我们就能使用线程分离,这样线程就能自己回收释放了,此时线程将由joinable变为onjoinable状态,此时的线程将不能被等待。
如果我们想在线程内部时自己detach,那么可以用pthread_self()获取自己的tid。
练习代码
#include<cstdio>
#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;
void* task(void* arg){
int count = 0;
const char* tmp = static_cast<const char*>(arg);
while(true){
printf("this is thread:%s\n",tmp);
count++;
if(count == 10) break;
sleep(1);
}
pthread_exit((void*)"我是新线程,我结束了");
//return (void*)"我是新线程,我结束了";
}
int main(){
pthread_t tid = 0;
pthread_create(&tid,nullptr,task,(void*)"this is new thread");
void* ret = nullptr;
int cnt = 5;
while(cnt--) sleep(1);
pthread_cancel(tid);
pthread_join(tid,&ret);
printf("return val :%d\n",ret);
return 0;
}
#include<cstdio>
#include<unistd.h>
#include<pthread.h>
#include<vector>
using namespace std;
// void* task(void* arg){
// const char* buffer = static_cast<const char*>(arg);
// while(true){
// printf("%s\n",buffer);
// sleep(1);
// }
// return nullptr;
// }
// int main(){
// #define NUM 10
// pthread_t array[NUM] ={0};
// for(int i = 0 ; i < NUM;i++ ){
// char buffer[1024];
// snprintf(buffer,sizeof buffer,"thread: %d",i);
// pthread_t tid = 0;
// pthread_create(&tid,nullptr,task,buffer);
// array[i] = tid;
// }
// for(int i = 0 ; i < NUM; i++){
// pthread_join(array[i],nullptr);
// }
// return 0;
// }
//上面的代码是错误的,会导致每个线程都是从9开始访问,因为其传的是地址,而每次for都会更改地址指向内容,而且线程进入的速度不同,导致异常情况
void* task(void* arg){
const char* buffer = static_cast<const char*>(arg);
int cnt = 10;
while (cnt--)
{
printf("%s\n",buffer);
sleep(1);
}
return 0;
}
struct thread{
pthread_t tid;
char buffer[1024];
};
int main(){
#define NUM 10
vector<thread*> v(NUM);
for(int i = 0 ; i < NUM;i++ ){
v[i] = new thread;
snprintf(v[i]->buffer,sizeof v[i]->buffer,"thread :%d",i);
pthread_t tid = 0;
pthread_create(&tid,nullptr,task,(void*)v[i]->buffer);
printf("%x\n",tid);
v[i]->tid = tid;
}
for(int i = 0 ; i < NUM; i++){
pthread_join(v[i]->tid,nullptr);
printf("on sucess:%x\n",v[i]->tid);
delete v[i];
}
return 0;
}
5.线程库,tid,独立栈,线程局部存储
我们编写的代码链接线程库时,需要-lphread指定链接的库才能链接成功。
当运行程序后,线程动态库将会加载到进程地址空间的共享区,线程库为我们提供了方法接口。线程的私有数据,例如线程的上下文结构,线程私有栈,线程的局部存储数据都是在mmap分配的。
线程的pthread_t tid是实际上是一个地址,其指向的是线程数据的起始位置
线程局部数据存储
例如我们定义了一个全局变量g_val,将其用__thread修饰,那么每个进程都会拥有其副本,这样的称为线程的局部数据
#include<cstdio>
#include<unistd.h>
#include<pthread.h>
#include<vector>
using namespace std;
// void* task(void* arg){
// const char* buffer = static_cast<const char*>(arg);
// while(true){
// printf("%s\n",buffer);
// sleep(1);
// }
// return nullptr;
// }
// int main(){
// #define NUM 10
// pthread_t array[NUM] ={0};
// for(int i = 0 ; i < NUM;i++ ){
// char buffer[1024];
// snprintf(buffer,sizeof buffer,"thread: %d",i);
// pthread_t tid = 0;
// pthread_create(&tid,nullptr,task,buffer);
// array[i] = tid;
// }
// for(int i = 0 ; i < NUM; i++){
// pthread_join(array[i],nullptr);
// }
// return 0;
// }
//上面的代码是错误的,会导致每个线程都是从9开始访问,因为其传的是地址,而每次for都会更改地址指向内容,而且线程进入的速度不同,导致异常情况
__thread int g_val = 10;
void* task(void* arg){
// const char* buffer = static_cast<const char*>(arg);
// int cnt = 10;
// while (cnt--)
// {
// printf("%s\n",buffer);
// sleep(1);
// }
printf("%x\n",&g_val);
return 0;
}
struct thread{
pthread_t tid;
char buffer[1024];
};
int main(){
printf("%x\n",&g_val);
#define NUM 10
vector<thread*> v(NUM);
for(int i = 0 ; i < NUM;i++ ){
v[i] = new thread;
snprintf(v[i]->buffer,sizeof v[i]->buffer,"thread :%d",i);
pthread_t tid = 0;
pthread_create(&tid,nullptr,task,(void*)v[i]->buffer);
//rintf("%x\n",tid);
v[i]->tid = tid;
}
for(int i = 0 ; i < NUM; i++){
pthread_join(v[i]->tid,nullptr);
//printf("on sucess:%x\n",v[i]->tid);
delete v[i];
}
return 0;
}
我们观察到每个线程的g_val的地址都不同。
6.线程与进程
进程是承担分配资源的基本实体,线程是进程中的执行流,是CPU调度的基本单位。
信号的作用实体是进程,会让每个PCB中的信号pending位改变,因此线程推出时并不会涉及到信号,因为当线程异常时,如果有信号,那么会杀死整个进程。