

**前引:**提到 Linux 并发,绕不开进程与线程的关系。很多人会把线程当成 "迷你进程",但二者的底层实现、调度机制和使用场景截然不同。本文不堆砌复杂术语,而是从 "定义、资源占用、调度方式" 三个核心维度,用通俗的语言拆解二者的区别与联系,让你彻底明白:线程到底是什么,以及它和进程如何配合支撑 Linux 系统的并发能力!
目录
【一】什么是线程
理解:"进程"有着自己完整的资源(比如代码数据、各个地址的分布...)线程好比进程内的一个个分支,可以调度"进程"内部的资源,来执行对应的分支任务------类比"厂房"中的"工人"利用进程的内部资源来执行不同的任务,之前学习的进程好比一个执行流,也就是一个线程,全部任务都由这个执行流完成;线程越多,该进程的效率越高,对应越复杂,风险越高!
本质:线程是共享进程资源的实体;进程就是分配系统资源的实体
概念:线程是进程的执行单元(或执行流),共享进程的资源、独立运行,本质是"轻量化进程"
【二】进程与线程的切换效率
如果要切换一个进程:
需要销毁对应的全部数据,最终也是去除PCB结构,然后重新加载新的结构数据,形成新的PCB
尤其是"热数据",这是进程间切换导致效率低的一大"痛点":最小都有几千KB
"热数据"是需要被高频读取、使用的数据,比如全局变量、函数这些,进程为了避免每次访问都需要去重新加载,就将这些"热数据"放在了Cache(CPU高速缓存区)供CPU快速找到该类型的数据
"热数据"对应着"冷数据",即哪些访问频率很低的数据,"热数据"需要跟随进程的访问情况随时替换
如果要切换一个线程:
线程是和进程共享资源的,线程的退出不需要去换进程PCB这些,只有进程的退出才去释放PCB,所以线程的替换与退出最极端也是加载一些新的代码数据到CPU执行,影响很低
【三】虚拟到物理地址的转换
在之前我们知道进程地址空间的虚拟地址转换到物理地址需要借助中间的页表,今天我们再深入!
比如现在有一个32位的虚拟地址:X00000001000000010000000100000001
此时虚拟地址会被按照10、10、12位被分隔开:0000000100 0000010000 000000000001
前10位对应**"页目录索引"** ;中间10位对应**"页表索引"** ;后12位对应**"业内偏离"**
先用前10位确定一级页表的位置,一级页表里面存着二级页表的起始位置(2^10=1024)
中间10位再确定二级页表的具体位置,二级页表中存内存中对应页框位置(2^10=1024)
最后12位再确定页框中具体的字节位置((2^12=4096=4KB))
(后12位:操作系统规定 "一页内存的大小是 4KB"(4096 字节),而 4096 = 2¹²,所以需要 12 位二进制才能表示 "一页内的所有位置"(0 到 4095)比如上面的后 12位 000000000001 换算成十进制是 1,意思是 "在这一页的第 1 个字节位置")

【四】线程库的认识
首先需要知道系统是提供了"线程库"的,即线程的系统调用接口,但是后来由于线程库的部分接口很复杂,用户二次封装,出现了用户层面二次封装的"线程库",我们主要学习用户层面的"线程库"
注意:Linux中是没有明确的"线程"的概念的,只有"轻量化进程"的概念,因此上面的线程内核接口实质是轻量化进程的接口。该线程原库名为**"Pthread库"**,几乎所有Linux平台都已默认携带
【五】线程使用基本常识
(1)任何一个线程如果出现错误会导致进程同时关闭
(2)线程是进程的执行分支,如果单个线程出现异常,操作系统也会向进程发送信号
导致该程和所有线程关闭
(3)既然线程是进程的执行分支,因此进程退了,该进程的所有线程自然也就崩了
(4)不能使用exit()终止线程,否则会直接导致整个进程结束,它是用来终止进程的
【六】线程库接口
(1)pthread_t
该接口用户返回系统成功创建线程的ID,我们需要创建一个变量接收,例如:tid
cpp
pthread_t tid;
(注意:此时只是获取了线程ID,并没有创建出来)
此时 tid 就接收了底层系统调用返回的线程 ID,例如:(注意需要指定使用线程库)



(2)pthread_create()
原型:
cpp
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
返回值:成功返回0;失败返回错误码(注意错误原因以及错误码都是线程库里面的)
作用:创建一个线程
参数:
**第一个参数:**获取的线程ID地址
**第二个参数:**设置线程的属性(这个后面再谈,先设置为NULL)
**第三个参数:**线程要执行任务的函数
- 函数的返回值是
void *(可以返回任意类型的数据,最后要强制转换) - 函数的参数是
void *(可以给函数传任意类型的数据,传之前要强制转换)
**第四个参数:**函数参数
理解:参数
下面我通过讲解普通函数(无参)、普通函数(一个参数)、传对象来教学如何使用这两个接口:
普通函数(无参):
假如现在有这样一个线程的任务函数:
cpp
void* thread_task(void* arg)
{
std::cout<<"Hello pthread"<<std::endl;
//注意函数是有返回值的
return NULL;
}
现在如果要利用pthread_create()创建线程调用该函数,应该这样使用:


解读:第三个参数由系统调用的函数指针指向任务的函数
第四个参数为任务函数的参数(指针类型),既然不需要函数参数就传NULL
普通函数(有参):
假如现在有这样一个线程的任务函数,需要传递一个字符串作为参数:
cpp
void* thread_task(void* arg)
{
//注意需要强转回来
std::cout<<(char*)arg<<std::endl;
//注意函数是有返回值的
return NULL;
}


解读:该函数的参数需要先强转为 void*,保持与函数原型一致(传参和函数的参数都必须是 void*),随后在使用的时候在强转回来
结构体参数:
假如有下面这样一个结构体对象:(不管怎样第三个参数只能是函数)
cpp
struct Person
{
Person(const char* ptr,int age)
:_ptr(ptr),_age(age)
{}
const char*_ptr;
int _age;
};
void* thread_task(void* arg)
{
//注意需要强转回来
std::cout<<(((struct Person*)arg)->_ptr)<<std::endl;
std::cout<<(((struct Person*)arg)->_age)<<std::endl;
//注意函数是有返回值的
return NULL;
}


解读:不管是传什么参数,都要求指针 且必须强转为 void* 类型 ,与函数原型一致,随后使用参 数的时候再强转回来
理解:返回值
返回值的原理都是一样的:先强转为(void*)指针返回,外面需要有一个接收参数的(void*)指针,同时需要告诉线程返回值存放的位置,拿到之后再强转回来使用
cpp
struct Person
{
Person(const char* ptr,int age)
:_ptr(ptr),_age(age)
{}
const char*_ptr;
int _age;
};
void* thread_task(void* arg)
{
//注意需要强转回来
struct Person* ptr=(struct Person*)arg;
//注意函数是有返回值的
return (void*)new struct Person(ptr->_ptr,ptr->_age);
}


(3)pthread_join()
原型:
cpp
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
参数:
thread :需要等待的子线程 ID(由 pthread_create 函数返回)
retval :用于存储子线程的返回值(即子线程函数 return 的指针)(无返回值就填NULL)
返回值:
- 成功:返回
0 - 失败:返回非零错误码
作用:阻塞等待子线程结束并获取其返回值(回收资源)
例如:

(4)pthread_exit()
原型:
cpp
#include <pthread.h>
void pthread_exit(void *retval);
参数:指向线程返回值的指针,用于传递线程的退出状态
作用:当前线程会立即停止执行,退出并进入 "终止状态",并可通过pthread_join获取返回值
例如:

(5)pthread_cancel()
原型:
cpp
#include <pthread.h>
int pthread_cancel(pthread_t thread);
参数:目标线程的 ID
作用:向目标线程发送取消请求,其核心是 "请求" 而非 "强制终止"
(6)pthread_self()
原型:
cpp
#include <pthread.h>
pthread_t pthread_self(void);
作用:获取当前线程的 ID(标识符)(类似getpid())
补充:我们还可以通过指令 ps -La获取所有线程ID(PID代表进程,LWP代表线程)

【七】性质验证
我先形成验证需要的代码:给线程开辟堆空间,用容器来存储结构体指针(形成三个线程为例)
cpp
#include<unistd.h>
#include<iostream>
#include<pthread.h>
#include<vector>
#define MAX 3
class My_Pthread
{
public:
My_Pthread(pthread_t id,int name)
:_id(id),_name(name)
{}
//线程ID
pthread_t _id=0;
//线程名
int _name;
};
//线程任务函数
void* handle(void* arg)
{
//防止线程跑太快
sleep(1);
//强转
My_Pthread* ptr=(My_Pthread*)arg;
//std::cout<<"线程编号"<<(ptr->_name)<<"线程ID:"<<(ptr->_id)<<std::endl;
return NULL;
}
int main()
{
//用指针来存取线程信息
std::vector<My_Pthread*> pthread_pointer;
for(int i=1;i<=MAX;i++)
{
pthread_t id;
//线程执行任务
My_Pthread* ptr=new My_Pthread(id,i);
//存储线程数据
pthread_pointer.push_back(ptr);
pthread_create(&id,NULL,handle,(void*)ptr);
ptr->_id=id;
}
//线程等待
for(int i=0;i<MAX;i++)
{
pthread_join(pthread_pointer[i]->_id,NULL);
}
//回收空间
for(int i=0;i<MAX;i++)
{
delete pthread_pointer[i];
}
return 0;
}
(1)验证:堆空间共享
创建一个全局的堆空间,然后每个线程填入自己的ID:

结果如下:

(2)验证:栈空间各自开辟
在线程函数内部设置一个变量,修改某一个线程的变量数据,看是否后面线程随着更改:

结果如下:

(3)验证:主线程可访问线程数据
在线程函数内部修改数据,外面用主线程访问:线程3的_date数据

结果如下:

(4)__thread关键字
__thread 的本质是让变量成为 "线程私有":
- 全局声明的
__thread变量,会为每个线程分配独立的内存空间(副本)- 线程内部对该变量的读写,操作的是自己的副本,与其他线程完全隔离
- 变量的生命周期与线程一致(线程创建时初始化,线程结束时自动销毁)

理想效果:每个线程访问数据段中的date全局变量时,都是从9000开始访问
测试结果:

