Linux线程 --- 1
1.什么是线程?我们认为,线程是操作系统进行调度的基本单位!2.什么是进程?进程 = 内核数据结构(task_struct) +代码和数据重新理解进程内核观点:进程是承担分配系统资源的基本实体进程 = (一大堆)内核数据结构(task_struct) + 代码和数据执行流是资源吗?--- 当然是线程就是进程内部的执行流资源如何理解我们以前的进程呢??操作系统以进程为单位,给我们分配资源,只不过我们
大家好!
提到并发编程,线程一定是重中之重。相比于笨重的进程,线程共享进程地址空间、创建开销更小、切换效率更高,也是现代操作系统主流的并发实现方案。
但在 Linux 环境下,线程的实现并没有想象中简单:我们代码中使用的 pthread_t 用户线程、内核调度的 LWP 轻量级进程,都是极易混淆的知识点。
本文从零梳理 Linux 线程核心原理,厘清用户级线程与内核级线程的区别,详解 Linux 经典的 1:1 线程映射机制,搭配代码 + 图片展示,零基础也能轻松看懂线程底层逻辑。
目录
1.线程概念
线程:是进程内的一个执行分支。线程的执行粒度,要比进程要细。
1.Linux中线程该如何理解?

进程访问内存时,需要通过地址空间和页表机制在物理内存中定位代码、数据和堆空间。
地址空间是进程访问资源的视图窗口,它定义了进程可见的所有资源范围。
理论上,通过让一个task_struct指向原进程的地址空间并共享部分代码数据及页表映射,可以创建多个这样的进程。这些新进程能够共享原始进程的地址空间,并将代码段划分为多个部分分别执行,从而实现内存资源的共享。与传统进程相比,这些新进程具有更细粒度的执行单元。
我们将这种机制实现的进程称为线程。
结论(Linux实现方案):
1.在Linux中,线程在进程“内部”执行,线程在进程的地址空间内运行(为什么?)
任何执行流执行,都要有资源!地址空间是进程的资源窗口,线程在进程的地址空间运行是不过分的
2.在Linux中,线程的执行粒度要比进程更细?线程执行进程代码的一部分。
2.重新定义线程和进程
1.什么是线程?
我们认为,线程是操作系统进行调度的基本单位!
2.什么是进程?
进程 = 内核数据结构(task_struct) + 代码和数据
重新理解进程
内核观点:进程是承担分配系统资源的基本实体
进程 = (一大堆)内核数据结构(task_struct) + 代码和数据

执行流是资源吗? --- 当然是
线程就是进程内部的执行流资源
如何理解我们以前的进程呢??
操作系统以进程为单位,给我们分配资源,只不过我们当前的进程内部只有一个执行流!
Linux 通过进程数据结构和管理算法来实现线程功能,具体体现在 struct task_struct 的设计上。由于 Linux 并未采用真正的线程概念,而是巧妙地使用进程内核数据结构来模拟线程。 --- 这种设计既简化了系统架构,又降低了维护成本,同时减少了出错概率。这一设计理念体现了卓越的系统工程智慧。
从CPU的视角来看,它无法区分线程和进程的区别,CPU所识别的执行单元始终是小于或等于进程级别的。具体关系可以表示为:线程 ≤ 执行流 ≤ 进程。
Linux中的执行流:轻量级进程
3.重谈地址空间
如何理解资源分配给线程?
虚拟地址是如何转换到物理地址的?32为虚拟地址为例

虚拟地址是32位
32位的虚拟地址不是一个整体,32位的虚拟地址是将它转换为10 + 10 + 12
页表也不是一整块儿的,如果是一整块的会怎么样呢?
页表的一行保存虚拟地址,物理地址和标志位,假设为4 + 4 + 2 = 10字节
若要计算页表的行数,需考虑整个虚拟地址空间的映射情况。假设采用32位地址空间(0x00000000到0xFFFFFFFF)且全部虚拟地址都建立映射,那么页表需要包含2^32个条目。
如果有2^32个字节的大小就要存4GB的内容,更不要说乘以10了,单独一个页表就存40GB那是不可能的
页表是被拆成两级的,第一级,只有1024(2^10)个条目
虚拟地址的前10位数直接转换成对应的十进制数作为一级页表的下标,找到二级页表。
在用中间的10位数转成十进制数,找到二级页表,其中存放的是物理内存中页框的起始地址
第一级页表叫做页目录,里面的内容叫做页目录表项,存放的是许多的二级页表的地址。
第二级页表里面的内容叫做页表表项,里面存放着对应页框的起始地址和一些权限的字段。
剩余的12个比特位表示的范围是[0, 2^12-1],用于指定从页框内访问物理内存的相对偏移量。由于一个页框的大小正好是4KB(即2^12字节),这个偏移量范围与页框容量完全匹配。

像这样的页表设计大小会是多大呢???
一个二级页表中内容是4个字节,页框的地址也并不需要2的32次方(4字节),20个比特位,2的20次方也就可以存放页框地址了。
对于1个二级页表,4字节大小乘以1024就是4kb的大小,可以放在一个页框。
页目录自己存放一个4kb大小的页框就够了。
页目录其中1024个表项,计算下来就是:4kb * 1024 = 4mb
对于一个进程,一般情况下都是不会将空间用光,对于二级页表大部分时间都是用不完的。
一个进程的页表算下来也不是特别大。
虽然大小很小了,但是对于创建一个进程也还是很一件很“重”的工作!!
对于CR3寄存器来讲,它是直接指向页目录的,任何一个进程,二级页表可以没有或者残缺,但是页目录是必须要存在的。
CR2寄存器,保存的是一些原因引起缺页中断或者异常的地址,保存在CR2寄存器里面,等把其他工作做好了,再用这个地址继续访问。
线程目前分配资源,本质就是分配地址空间的范围。
4.Linux线程周边的概念
线程 VS 进程
进程是自愿分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也有自己的一部分数据:
线程ID、一组寄存器、栈、errno、信号屏蔽字、调度优先级
进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调度,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享一下进程资源和环境:
文件描述符表、每种信号的处理方式、当前工作目录、用户id和组id
线程比进程要更轻量化,为什么?
a.创建和释放更加轻量化(生死)
b.切换更加轻量化(运行)
线程在进行切换的时候,页表和地址空间都不需要切换,所以线程在切换的时候更加高效一些
但是就只是多几个寄存器,少几个寄存器的问题,这就提高效率了吗?
线程的执行本质就是进程在执行,毕竟线程是进程的执行分支
CPU中除了寄存器,还有一个缓存叫cache,缓存的热数据,将高频的数据缓存到里面。
线程在切换的时候,上下文不变化,数据缓存的热数据就不需要保存了。
CPU调度的基本单位是线程,并不代表进程不需要调度。操作系统也要调度进程。
进程在切换的时候,数据要保存,但是cache直接被舍弃了,新的cache需要从冷到热。
线程的cache不需要由冷到热重新缓存。
缓存的大小是比寄存器大很多的,单位都是kb的

其实一个线程在被创建的时候,也有自己的时间片,它的时间片也是来自于对应的进程的。时间片也是资源,所以创建每一个执行流线程,不能给线程重新申请时间片,必须把整个进程的时间片平均划分给线程
5.线程的优点
创建一个新线程的代价要比创建一个新进程的代价小得多
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
线程占用的资源比进程少很多
能充分利用多处理器的可并行数量
在等待慢速I/O操作结束的同时,程序可以执行其他的计算任务
I/O密集型应用,为了提高性能,将I/O操作重叠,线程可以同时等待不同的I/O操作
2.线程创建
pthread库
Linux中是通过进程模拟线程,所以在Linux内核中有没有很明确的线程概念呢?没有,只有轻量级进程的概念。注定了不会直接给我们提供线程的系统调用,只会给我们提供轻量级进程的系统调用。
我们用户,需要线程的接口!
所以就有了程序员开发出来了pthread线程库 --- 应用层 --- 轻量级进程接口进行封装,为用户提供直接线程的接口。
几乎所有的Linux平台,都是默认自带这个库的!
Linux中编写多线程代码 需要使用第三方pthread库!!
pthread_create
pthread_create 创建一个新的线程
头文件:pthread.h

对于第三个参数,函数指针:线程在创建的时候,每一个执行流执行代码的一部分,自己编程写代码的时候,想让线程执行哪部分,就传入对应的函数指针。main函数是主线程执行的入口函数,这里就是新线程执行的入口函数

返回值,0代表成功,非0代表错误,通过返回值的方式告诉错误码是几

由于这是一个库方法,在编译的时候,需要加上-lpthread才能正常的链接对应的库,使用这个方法。

LWP
线程是调度的基本单位,每一个线程都是在同一个地址空间当中,所有的线程都属于同一个进程,所有线程去调用getpid打印出来的是同一个pid:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// new thread
void* threadRoutine(void* args)
{
while(true)
{
cout << "new thread, pid: " << getpid() << endl;
sleep(2);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, nullptr);// 不是系统调用
// 库方法,
while(true)
{
cout << "main thread, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}

可是操作系统选择线程的时候,它怎么知道哪个线程是主线程,每个线程都是调度的基本单元,每个线程是不是要有自己的id值呢?

通过ps -aL查看(-L)大L选项,查看当前用户启动的所有轻量级进程

对应的LWP是什么?
这就是相应的轻量级进程PID,这些轻量级进程的调度正是基于这个PID来实现的。
通过LWP和PID是否相等就可以决定是否是主线程!!
线程简单使用展示
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;
// 可以被多个执行流同时执行,show函数被重入了!!
void show(const string& name)
{
cout << name << "say* " << "hello thread" << endl;
}
// new thread
void* threadRoutine(void* args)
{
while(true)
{
// cout << "new thread, pid: " << getpid() << endl;
show("[new thread]");
sleep(2);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, nullptr);// 不是系统调用
// 库方法,
while(true)
{
// cout << "main thread, pid: " << getpid() << endl;
show("[main thread]");
sleep(1);
}
return 0;
}

这里的show函数被多个执行流重入了(有线程安全)
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;
int g_val = 100;
// 可以被多个执行流同时执行,show函数被重入了!!
void show(const string& name)
{
cout << name << "say* " << "hello thread" << endl;
}
// new thread
void* threadRoutine(void* args)
{
while(true)
{
printf("new thread, pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);
// cout << "new thread, pid: " << getpid() << endl;
// show("[new thread]");
sleep(2);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, nullptr);// 不是系统调用
// 库方法,
while(true)
{
printf("main thread, pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);
// cout << "main thread, pid: " << getpid() << endl;
// show("[main thread]");
sleep(1);
g_val++;
}
return 0;
}

未初始化,已初始化的全局变量在所有的线程中都是共享的!
线程中通信就非常方便
线程出错,进程停止
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;
int g_val = 100;
// 可以被多个执行流同时执行,show函数被重入了!!
void show(const string& name)
{
cout << name << "say* " << "hello thread" << endl;
}
// new thread
void* threadRoutine(void* args)
{
while(true)
{
printf("new thread, pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);
// cout << "new thread, pid: " << getpid() << endl;
// show("[new thread]");
sleep(5);
int a = 10;
a /= 0;
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, nullptr);// 不是系统调用
// 库方法,
while(true)
{
printf("main thread, pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);
// cout << "main thread, pid: " << getpid() << endl;
// show("[main thread]");
sleep(1);
g_val++;
}
return 0;
}


可以看到只要线程发生意外终止,整个进程就会终止,所有线程就都要停止,监视窗口也直接打印不出来了
3.线程控制
pthread_t tid
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;
int g_val = 100;
// 可以被多个执行流同时执行,show函数被重入了!!
void show(const string &name)
{
cout << name << "say* " << "hello thread" << endl;
}
// new thread
void *threadRoutine(void *args)
{
while (true)
{
printf("new thread, pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, nullptr); // 不是系统调用
// 库方法,
while (true)
{
printf("main thread, pid: %d, g_val: %d, &g_val: 0x%p, create new thread tid : % ul\n ",
getpid(),
g_val, &g_val, tid);
sleep(1);
g_val++;
}
return 0;
}


可以看到tid和前面说的那个轻量级进程pid好像不一样?
这个tid无符号整数究竟是什么呢?
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;
int g_val = 100;
// 可以被多个执行流同时执行,show函数被重入了!!
void show(const string &name)
{
cout << name << "say* " << "hello thread" << endl;
}
// new thread
void *threadRoutine(void *args)
{
while (true)
{
printf("new thread, pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, nullptr); // 不是系统调用
// 库方法,
while (true)
{
printf("main thread, pid: %d, g_val: %d, &g_val: %p, create new thread tid : %p\n ",
getpid(),
g_val, &g_val, tid);
sleep(1);
g_val++;
}
return 0;
}

我们使用%p打印,可以看出这个tid是一个地址!
- 本质:进程虚拟地址空间中线程控制块(TCB)的内存地址(通常是
unsigned long类型) - 作用:在当前进程内部唯一标识一个线程,用于
pthread_join、pthread_cancel等库函数操作
pthread_t tid和 LWP的区别
-
pthread_t tid(用户态)- 全称:POSIX 线程 ID(Thread ID)
- 管理者:
pthread线程库(glibc/NPTL) - 本质:进程虚拟地址空间中线程控制块(TCB)的内存地址(通常是
unsigned long类型) - 作用:在当前进程内部唯一标识一个线程,用于
pthread_join、pthread_cancel等库函数操作
-
LWP (Light Weight Process,内核态)
- 全称:轻量级进程 ID(也常直接称为 TID, Thread ID)
- 管理者:Linux 内核
- 本质:内核为每个可调度任务(
task_struct)分配的全局唯一整数 - 作用:内核调度 CPU、分配资源、跟踪任务的唯一标识
线程等待
一个新线程创建出来了是主线程先跑还是新线程先跑??? --- 不确定
谁最后退出呢??? --- 主线程
创建线程的本质就是对线程的管理,要保证主线程最后退出。
新线程退出,默认也会导致类似僵尸进程的问题。防止新线程内存泄漏,和获取退出的内容。所以需要线程等待
pthread_join
pthread_join:等待一个进程


#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;
int g_val = 100;
// 可以被多个执行流同时执行,show函数被重入了!!
void show(const string &name)
{
cout << name << "say* " << "hello thread" << endl;
}
// new thread
void *threadRoutine(void *args)
{
const char* name = (const char*) args;
int cnt = 5;
while (true)
{
printf("%s, pid: %d, g_val: %d, &g_val: %p\n", name, getpid(), g_val, &g_val);
sleep(1);
cnt--;
if (cnt == 0) break;
}
return nullptr;// 运行到这里默认线程退出了!!
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1"); // 不是系统调用
sleep(10);
pthread_join(tid, nullptr);// main thread等待的时候,默认是阻塞等待的!
cout << "main thread quit ..." << endl;
return 0;
}

![]()

可以看到等待是必须的,新线程结束,主线程还是会有存在的。
pthread_join是阻塞等待
pthread_join第二参数
线程执行完,怎么得到线程执行函数的返回值呢?使用第二个参数void** retval即可实现需求~

我们只要再创建一个void* reval,传入它的&reval,就可以将返回值void*给拿出来放入reval中:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;
int g_val = 100;
// 可以被多个执行流同时执行,show函数被重入了!!
void show(const string &name)
{
cout << name << "say* " << "hello thread" << endl;
}
// new thread
void *threadRoutine(void *args)
{
const char* name = (const char*) args;
int cnt = 5;
while (true)
{
printf("%s, pid: %d, g_val: %d, &g_val: %p\n", name, getpid(), g_val, &g_val);
sleep(1);
cnt--;
if (cnt == 0) break;
}
return (void*)1;// 将整数1转成void*
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
void* retval;
pthread_join(tid, &retval);// 传入二级指针
cout << "main thread quit ..., retval: " << (long long)retval << endl;
return 0;
}

这里要使用long long(8字节)去强转回来打印,不能用int(4字节),因为测试过里面是64位环境,指针大小为8字节。不然会发生报错。
为什么我们在join这里的时候不考虑异常呢?
因为做不到,线程出异常了,代表整个进程就会异常。
线程退出
除了用return 返回退出线程,还有什么呢?--- exit函数可以吗?
如果通过线程来调用exit来退出呢?会怎么样?
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;
int g_val = 100;
// 可以被多个执行流同时执行,show函数被重入了!!
void show(const string &name)
{
cout << name << "say* " << "hello thread" << endl;
}
// new thread
void *threadRoutine(void *args)
{
const char* name = (const char*) args;
int cnt = 5;
while (true)
{
printf("%s, pid: %d, g_val: %d, &g_val: %p\n", name, getpid(), g_val, &g_val);
sleep(1);
cnt--;
if (cnt == 0) break;
}
exit(11);// 通过新线程调用exit?
// return (void*)1;// 将整数1转成void*
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
void* retval;
pthread_join(tid, &retval);// 传入二级指针
cout << "main thread quit ..., retval: " << (long long)retval << endl;
return 0;
}

我们会发现进程直接就截止了?exit是用来终止进程的,不能用来终止线程。
pthread_exit
通过pthread_exit退出
void *threadRoutine(void *args)
{
const char* name = (const char*) args;
int cnt = 5;
while (true)
{
printf("%s, pid: %d, g_val: %d, &g_val: %p\n", name, getpid(), g_val, &g_val);
sleep(1);
cnt--;
if (cnt == 0) break;
}
pthread_exit((void*)100);
// exit(11);// 通过新线程调用exit?
// return (void*)1;// 将整数1转成void*
}

所以终止线程的第二种方法:pthread_exit
显然的如果是main函数这个主线程直接return退出,线程也将会直接退出...
pthread_cancel
还有一种退出线程的方法:
线程取消 pthread_cancel

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;
int g_val = 100;
// 可以被多个执行流同时执行,show函数被重入了!!
void show(const string &name)
{
cout << name << "say* " << "hello thread" << endl;
}
// new thread
void *threadRoutine(void *args)
{
const char* name = (const char*) args;
int cnt = 5;
while (true)
{
printf("%s, pid: %d, g_val: %d, &g_val: %p\n", name, getpid(), g_val, &g_val);
sleep(1);
cnt--;
if (cnt == 0) break;
}
pthread_exit((void*)100);
// exit(11);// 通过新线程调用exit?
// return (void*)1;// 将整数1转成void*
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
sleep(1);// 为了保证新线程已经启动
pthread_cancel(tid);// 取消目标线程
void* retval;
pthread_join(tid, &retval);// 传入二级指针
cout << "main thread quit ..., retval: " << (long long)retval << endl;
return 0;
}

会发现直接将线程取消了,它会存在一个宏PTHREAD_CANCEL,这个被设置返回。它的值就是(void*)-1
重谈线程参数和返回值
传入线程的参数可以更加丰富,比如传入类的对象进入
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
using namespace std;
// 控制线程的时候,可以传更多的东西
class Requst
{
public:
Requst(int start, int end, const string& threadname)
: _start(start)
, _end(end)
, _threadName(threadname)
{}
public:
int _start;
int _end;
string _threadName;
};
class Response
{
public:
Response(int result, int exitcode)
: _result(result)
, _exitcode(exitcode)
{}
public:
int _result;// 计算结果
int _exitcode;// 计算结果是否可靠
};
void* sumCount(void*args)
{
Requst* rq = static_cast<Requst*>(args);//相关类型的隐式类型转换 c++
Response* rsp = new Response(0, 0);
for (int i = rq->_start; i <= rq->_end; i++)
{
cout << rq->_threadName << " is running, calcing ..., " << i << endl;
rsp->_result += i;
usleep(100000);// 0.1秒
}
delete rq;
return rsp;
}
int main()
{
pthread_t tid;
Requst* rq = new Requst(1, 100, "thread 1"); // 1到100求和
pthread_create(&tid, nullptr, sumCount, rq);
void* ret;
pthread_join(tid, &ret);
Response* rsp = static_cast<Response*>(ret);
cout << "rsp->result: " << rsp->_result <<
" , exitcode: " << rsp->_exitcode << endl;
delete rsp;
return 0;
}

可以发现线程的参数和返回值,不仅仅可以用来进行传递一般参数,也可以传递对象!
以后设计的时候,可以将任务拆分成若干子任务,然后分别让它们运行。
前面的代码,都是通过主线程在堆上创建空间来使用的,可以知道,堆空间也是被线程共享的。
c++11支持多线程
目前,我们的原生线程库,pthread库
不仅仅可以通过pthread_creat创建线程,
目前c++11语言本身也已经支持多线程了,它和原生线程库的关系?
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
#include <thread>
using namespace std;
void threadRun()
{
while(true)
{
cout << "I am a new thread for C++" << endl;
sleep(1);
}
}
int main()
{
thread t1(threadRun);
t1.join();
return 0;
}

不仅要加thread头文件,还有编译选项的-lpthread也不能省略掉,不然就会报错
所以在c++11里面的多线程就是封装的原生线程库pthread
在windows的平台下,有自己的一套线程块。c++也就是用的windows那一套实现的。
c++就具有其可移植性,跨平台性。
线程tid
pthread_self

pthread_self获取自己当前线程tid
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
#include <thread>
using namespace std;
string toHex(pthread_t tid)
{
// 10进制转成16进制
char hex[64];
snprintf(hex, sizeof(hex), "%p", tid);
return hex;
}
void* threadRoutine(void* args)
{
int cnt = 5;
while(true)
{
cout << "thread id: " << toHex(pthread_self()) << endl;
sleep(1);
cnt--;
if (cnt == 0) break;
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
cout << "main thread create thread done, new thread id: " << toHex(tid) << endl;
pthread_join(tid, nullptr);
return 0;
}

Linux中没有明确的线程概念,只有轻量级进程的概念
是如何创建轻量级进程的呢?


除了主线程,所有其他线程的独立栈,都在共享区具体来讲是在pthread库中,tid指向的用户tcb中!
1:1 模型(Linux 用的)
用户线程:内核 LWP = 1 : 1
- 优点:一个线程阻塞(sleep/IO),其他线程照常跑
- 缺点:创建线程要陷内核、开销稍大
- 就是你现在用的 pthread
验证
线程栈区独立
#include <iostream>
#include <pthread.h>
#include <vector>
#include <unistd.h>
using namespace std;
// 证明了线程的栈区是独立的
#define NUM 10
struct ThreadData
{
string _threadname;
};
string toHex(pthread_t tid)
{
char buffer[128];
snprintf(buffer, sizeof(buffer), "0x%x", tid);
return buffer;
}
// 所有的线程,执行的都是这个函数!
void* threadRoutine(void* args)
{
int test_i = 0;
ThreadData* td = static_cast<ThreadData*>(args);
int i = 0;
while(i < 10)
{
cout << "pid: " << getpid() <<
", tid: " << toHex(pthread_self()) <<
", threadname: " << td->_threadname <<
", test_i: " << test_i <<
", &test_i: " << &test_i << endl;
test_i++;
i++;
}
delete td;
return nullptr;
}
void InitThreadData(ThreadData* td, int number)
{
td->_threadname = "thread-" + to_string(number);
}
int main()
{
vector<pthread_t> tids;
// 创建一批线程
for (int i = 0; i < NUM; i++)
{
ThreadData* td = new ThreadData;
pthread_t tid;
InitThreadData(td, i);
pthread_create(&tid, nullptr, threadRoutine, td);
tids.push_back(tid);
sleep(1);
}
// 等待
for (int i = 0; i < tids.size(); i++)
{
pthread_join(tids[i], nullptr);
}
return 0;
}
#include <iostream>
#include <pthread.h>
#include <vector>
#include <unistd.h>
using namespace std;
// 主线程的栈在栈区里面,新线程的栈在库里面
// 但都是在地址空间里面,主线程想访问任何一个线程都是可以的,怎么验证呢
// 每一个线程都会有自己的独立栈结构
// 线程和线程之间几乎没有密码,线程的栈上的数据,也是可以被其他线程看到和访问的。
int* p = NULL;
#define NUM 10
struct ThreadData
{
string _threadname;
};
string toHex(pthread_t tid)
{
char buffer[128];
snprintf(buffer, sizeof(buffer), "0x%x", tid);
return buffer;
}
// 所有的线程,执行的都是这个函数!
void* threadRoutine(void* args)
{
int test_i = 0;
ThreadData* td = static_cast<ThreadData*>(args);
if (td->_threadname == "thread-2") p = &test_i;
int i = 0;
while(i < 10)
{
cout << "pid: " << getpid() <<
", tid: " << toHex(pthread_self()) <<
", threadname: " << td->_threadname <<
", test_i: " << test_i <<
", &test_i: " << &test_i << endl;
test_i++;
i++;
}
delete td;
return nullptr;
}
void InitThreadData(ThreadData* td, int number)
{
td->_threadname = "thread-" + to_string(number);
}
int main()
{
vector<pthread_t> tids;
// 创建一批线程
for (int i = 0; i < NUM; i++)
{
ThreadData* td = new ThreadData;
pthread_t tid;
InitThreadData(td, i);
pthread_create(&tid, nullptr, threadRoutine, td);
tids.push_back(tid);
sleep(1);
}
sleep(3);// 确保复制成功
cout << "main thread get a thread local value, val: " << *p <<
", &val: " << p << endl;
// 等待
for (int i = 0; i < tids.size(); i++)
{
pthread_join(tids[i], nullptr);
}
return 0;
}
全局变量所有线程共享
#include <iostream>
#include <pthread.h>
#include <vector>
#include <unistd.h>
using namespace std;
#define NUM 10
int g_val = 100; // 全局变量是被所有的线程同时看到并访问的!
struct ThreadData
{
string _threadname;
};
string toHex(pthread_t tid)
{
char buffer[128];
snprintf(buffer, sizeof(buffer), "0x%x", tid);
return buffer;
}
// 所有的线程,执行的都是这个函数!
void* threadRoutine(void* args)
{
ThreadData* td = static_cast<ThreadData*>(args);
int i = 0;
while(i < 10)
{
cout << "pid: " << getpid() <<
", threadname: " << td->_threadname <<
", tid: " << toHex(pthread_self()) <<
", g_val: " << g_val <<
", &g_val: " << &g_val << endl;
i++;
g_val++;
}
delete td;
return nullptr;
}
void InitThreadData(ThreadData* td, int number)
{
td->_threadname = "thread-" + to_string(number);
}
int main()
{
vector<pthread_t> tids;
// 创建一批线程
for (int i = 0; i < NUM; i++)
{
ThreadData* td = new ThreadData;
pthread_t tid;
InitThreadData(td, i);
pthread_create(&tid, nullptr, threadRoutine, td);
tids.push_back(tid);
sleep(1);
}
sleep(1);
// 等待
for (int i = 0; i < tids.size(); i++)
{
pthread_join(tids[i], nullptr);
}
return 0;
}
线程局部存储
如果想要私有的全局变量呢???
全局变量前面加上__thread
这种技术叫做线程的局部存储!!

__thread不是c/c++提供的,是编译器提供的编译选项。
发现打印出的地址都不在是全局区的地址了。


__thread定义线程的局部存储只能用于定义内置类型!不能用来修饰自定义类型
一般这个定义出来,将线程比较关心的一些系统调用的值先拿出来,就不用在大量循环中反复拿取。
那为什么不使用线程的栈去定义呢,然后去获取呢?它其实有些类似全局变量的作用那般,如果又调用了其他函数,不得再传入这个参数吗?所以是有必要有这个线程局部存储的。
分离线程 pthread_detach
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出后,自动释放线程资源
int pthread_detach(pthread_t thread);
如果分离之后,等待就不会等到了,就直接返回退出了,主线程直接退出来了,其他线程也会退出。所以要保证主线程不要退出,或者最后一个退出。
分离就是线程的一种属性状态,一个线程是否被分离,一定是在被记录下来的。本质分离就是在对应的原生线程库里面将对应的属性改变,表示是否能被join等待。
结语
很高兴大家能够看完这篇文章,其中的内容很多,但是也为我们揭开了线程神奇的面纱。相信看完这篇内容后各位能够收获满满的!有关后续的内容最近就会全部完善好一起发布的。
多多点赞 + 收藏 + 关注!!谢谢大家❤️

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)