目录
前文
回望页表
一,什么是线程
二,使用
pthread_create (线程创建)
三,线程控制
1 ,线程共享进程数据,但也拥有自己的一部分数据:
2, 线程 VS 进程优点
3,pthread_join(等待线程)
4,pthread_exit (线程终止)
5, pthread_cancel (线程取消)
6. pthread_t 类型
7. pthread_detach (线程分离)
四,线程互斥
1. 相关背景概念
2. 互斥量
1),初始化互斥量
2),互斥量加锁与解锁
3),销毁互斥量
理解锁
补充重入 & 线程安全概念
3. 常见的线程不安全的情况
常见的线程安全的情况
常见不可重入的情况
常见可重入的情况
可重入与线程安全联系
可重入与线程安全区别
嘿!收到一张超美的风景图,希望你每天都能顺心!
前文
结论:在磁盘中储存着的程序文件,他们其实已经被分成许多份大小为4KB的小空间块(被称为页帧);同时,物理内存中,数据以4KB为单位进行储存(被称作页框)。
当进行IO操作时,例如,向物理内存中导入数据,以4KB形式传递。
回望页表
在曾经,我们学习页表时,只是简单提了一下,今天我们再看页表,了解地更详细。
一,什么是线程
这样子,我们终于能理解这句话了:线程在进程内部执行,同时也是OS调度的基本单位。线程在进程的地址空间中运行,CPU不关心执行的是否是进程还是线程,只要PCB来执行就行。 值得注意的是,Linux只提供轻量级进程,通过pthread库来实现多线程功能!!Windows对多线程会进行数据结构管理维护,两者方案不同。
说到这里请让我们来重新理解进程:用户视角:进程由 内核数据结构 + 代码和数据,同之前的理解差不多,只是内核数据结构从之前的一个PCB成了多个PCB。内核视角:进程: 进程向OS申请空间,承担系统资源的基本实体。
CPU视角:Linux下,PCB <= 其他系统的PCB。因为linux多线程实现是分配同一个进程的资源,当进程只有一个线程才 ”等于“ 我们曾经所写的代码。话说到这里,有人会问Linux拥有真正的线程吗?? 答案:没有,多线程只是实现的的一种功能。Linux没有对线程组织管理的数据结构,是轻量级的进程,Linux通过PCB模拟了多线程的功能,同时我们也只有轻量级进程接口。
那怎么实现多线程功能呢??? 用 pthread 线程库——Linux自带的原生线程库
二,使用
pthread_create (线程创建)
thread: 返回线程 IDattr: 设置线程的属性, attr 为 NULL 表示使用默认属性start_routine: 是个函数地址,线程启动后要执行的函数arg: 传给线程启动函数的参数返回值:成功返回 0 ;失败返回错误码
pthread库,是用用户层的第三方库,不属于C/C++库,所以我们在编译时,需要额外链接pthread库(-pthread)。
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
void *func(void* str)
{
while (1)
{
cout << "new pthread play..., pid: " << getpid() << endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t pt[5];
for (int i = 0; i < 5; i++)
{
pthread_create(pt + i, nullptr, func, (void* )"victor");
}
while (1)
{
cout << "main pthread play ..., pid:" << getpid() << endl;
sleep(1);
}
return 0;
}
代码是有了,我们如何查看是否有这么多的线程呢???走
PS -aL | grep Thread // 就如我们前面一样的来查看线程
这里同样也验证了,我们之前的话,当一个进程只有一个线程时,其线程也可理解为进程。(线程标号 == PID)
三,线程控制
1 ,线程共享进程数据,但也拥有自己的一部分数据:
1. 独立的线程ID2. 一组寄存器(需要有寄存器来储存,线程上下文)3. 独立的栈 (比如说存储产生的临时变量)4. errno5. 信号屏蔽字6. 调度优先级
2, 线程 VS 进程优点
- 创建一个新线程的代价要比创建一个新进程小得多。(代价小)
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。(切换成本低)
- 线程占用的资源要比进程少很多。
- 能充分利用多处理器的可并行数量。
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
这里解释一下,线程较进程切换代价小的原因:CPU内部有 L1~L3cache(缓存),每当CPU读取数据时,会向内存中读取,并利用局部性原理用缓存记录下来那周围一部分数据;如果只是切换线程,由于线程之间共享代码和一些数据,那么就大概率CPU能在内部找到所需数据,不需要再次寻址,载入缓存。而如果是切换进程,需要保存旧进程数据,需要重新加载CPU缓存,效率自然就慢下来。
3,pthread_join(等待线程)
功能: 可以阻塞主线程,等待目标线程返回。
thread: 线程 IDvalue_ptr: 它指向一个指针,后者指向线程的返回值返回值:成功返回 0 ;失败返回错误码
retval : 线程结束返回值。那线程出现异常怎么办,答案是:不用关心线程是否出现异常,因为线程一旦出现崩溃,其他线程一同崩溃,进程也崩溃。
成功返回0; 失败,返回错误码。
4,pthread_exit (线程终止)
这里为什么不使用 exit()函数呢?? 原因是exit() 是进程退出!!而这个是线程退出;execl进程替换也不能在线程中随意使用,execl一旦第一进程进行替换,进程中的代码数据也将被替换,线程也无法继续执行。
1. 如果 thread 线程通过 return 返回 ,value_ ptr 所指向的单元里存放的是 thread 线程函数的返回值。2. 如果 thread 线程被别的线程调用 pthread_ cancel 异常终掉 ,value_ ptr 所指向的单元里存放的是常数 PTHREAD_CANCELED。3. 如果 thread 线程是自己调用 pthread_exit 终止的 ,value_ptr 所指向的单元存放的是传给 pthread_exit 的参数。4. 如果对 thread 线程的终止状态不感兴趣 , 可以传 NULL 给 value_ ptr 参数
5, pthread_cancel (线程取消)
thread: 也就是取消线程ID
当一个线程被取消,那么线程退出码,将被设置为PTHREAD_CANCELED(底层就是返回(void*)-1)
一般都是用于:主线程取消副线程的场景
实践一下:
void *func(void* str)
{
int n = 5;
int *data = new int[5];
while (n--)
{
cout << "new pthread play..., pid: " << getpid() << endl;
data[n] = n;
sleep(1);
}
pthread_exit((void*)111);
}
int main()
{
pthread_t pt;
pthread_create(&pt, nullptr, func, (void* )"victor");
sleep(3);
pthread_cancel(pt); // 取消线程
cout << "pthread_cancel get " << endl;
sleep(5);
int* ret = nullptr;
pthread_join(pt, (void**)&ret); // 等待线程退出
cout << "main pthread play ..., pid:" << getpid() << " ret :" << (long long )ret << endl;
return 0;
}
6. pthread_t 类型
所以我们所打印新线程的地址是共享内存位置的地址。
另外,在线程中,我们也可以获取当前线程的ID:pthread_self()。
上图中线程局部存储又是什么?? 答:被__thread 修饰的全局变量
__thread int tmp = 0; // __thread的结果,让每个线程都有自己被修饰的全局变量,这也是线程局部存储
7. pthread_detach (线程分离)
pthread_detach(pthread_self()) ;
void *func(void* str)
{
pthread_detach(pthread_self());
pthread_exit((void*)111);
}
int main()
{
pthread_t pt;
pthread_create(&pt, nullptr, func, (void* )"new pthread ");
sleep(1); // 需要等线程分离后,才可进行join等待
int* ret = nullptr;
cout << "main thread " << endl;
int n = pthread_join(pt, (void**)&ret); // 等待线程退出
cout << "n : " << n << " error : " << strerror(n) << endl;
return 0;
}
疑问:既然一个新线程已经分离了,那如果发生异常,是否会影响整个进程呢?
答案: 会的,因为线程依旧是共享着进程资源,如果分离的线程出现异常,依旧会导致整个进程发生退出(崩溃)
四,线程互斥
1. 相关背景概念
通过下面代码,我们来理解为什么需要线程互斥:
int ticket = 1000;
void* getticket(void* str)
{
// 打印并进行取票
while(ticket > 0)
{
usleep(1000);
cout << "getticket : " << ticket << endl;
ticket--;
}
pthread_exit(nullptr);
}
int main()
{
pthread_t t1, t2, t3,t4;
pthread_create(&t1, nullptr, getticket, nullptr);
pthread_create(&t2, nullptr, getticket, nullptr);
pthread_create(&t3, nullptr, getticket, nullptr);
pthread_create(&t4, nullptr, getticket, nullptr);
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
pthread_join(t4, nullptr);
return 0;
}
load : 将共享变量 ticket 从内存加载到寄存器中update : 更新寄存器里面的值,执行 -1 操作store : 将新值,从寄存器写回共享变量 ticket 的内存地址
总之:
票出现负数原因:我们的getticket函数是可重入函数。由于CPU调度切换原因导致,数据出现异步。
大部分情况,线程使用的数据都是局部变量(比如说栈上的变量),变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。 多个线程并发的操作共享变量,会带来一些问题。
不太理解数据异步可以参考下面这个例子:
2. 互斥量
为了解决下面的问题:
接口:
1),初始化互斥量
pthread_mutex_t 类型,本质上是一个联合体。
两种方法:
方法1,静态分配(全局锁):
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
全局锁,可以不用考虑销毁锁。
int pthread_mutex_init (pthread_mutex_t * restrict mutex, const pthread_mutexattr_t * restrict attr);
通过函数设置的锁,在 生命周期快结束时,需要销毁局部锁。
2),互斥量加锁与解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);int pthread_mutex_unlock(pthread_mutex_t *mutex);返回值: 成功返回 0, 失败返回错误号。
3),销毁互斥量
改善后的抢票系统:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int ticket = 1000;
void* getticket(void* str)
{
// 打印并进行取票
while(1)
{
pthread_mutex_lock(&mutex); // 加锁
if (ticket > 0 )
{
usleep(1000);
ticket--;
pthread_mutex_unlock(&mutex); // 解锁
// 为什么要将cout代码置出临界区??
// 我们需要注重临界区代码的颗粒度,颗粒度越小,越好。
// 对于多线程访问临界资源,我们需要考虑效率问题。将无关紧要的代码优化掉,提高临界区的颗粒,细密度。
cout << (char*)str << " getticket : " << ticket << endl;
}else
{
pthread_mutex_unlock(&mutex);
break;
}
pthread_exit(nullptr);
}
int main()
{
pthread_t t1, t2, t3,t4;
pthread_create(&t1, nullptr, getticket, (void*)"线程一:");
pthread_create(&t2, nullptr, getticket, (void*)"线程二:");
pthread_create(&t3, nullptr, getticket, (void*)"线程三:");
pthread_create(&t4, nullptr, getticket, (void*)"线程四:");
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
pthread_join(t4, nullptr);
return 0;
}
错误代码反思分享:
我们知道上面是对临界资源进行修改。那我们仅仅是访问临界资源呢??那我们是否可以不用跟其他线程进行竞争锁,直接访问!
回答:从语法上来说是允许的。但这是错误的编码思想。即使是访问临界资源,也需要进行申请锁。
理解锁
1. 对临界区代码进行加锁,使临界区的执行是穿行的。即使是CPU调度,持有锁的线程被换下,其他线程也执行不了临界区代码。
2. 多个线程线程竞争锁,锁本身也是一种共享资源,那如何保护锁的安全性? 答:申请锁与释放锁,底层执行操作是原子操作。
3. 锁的底层原理:
补充重入 & 线程安全概念
3. 常见的线程不安全的情况
不保护共享变量的函数函数状态随着被调用,状态发生变化的函数返回指向静态变量指针的函数调用线程不安全函数的函数
常见的线程安全的情况
常见不可重入的情况
常见可重入的情况
可重入与线程安全联系
可重入与线程安全区别
下期:多线程——下篇
结语
本小节就到这里了,感谢小伙伴的浏览,如果有什么建议,欢迎在评论区评论,如果给小伙伴带来一些收获请留下你的小赞,你的点赞和关注将会成为博主创作的动力。