执行流视角是如何看待CPU寄存器的?
CPU内部的寄存器本质,叫做当前执行流的上下文!寄存器空间是被所有的执行流共享的,但是寄存器的内容,是被每一个执行流私有的!
图解互斥锁
假设共识:
- 将互斥锁变量mutex理解成一个整形变量,某进程cpu上下文寄存器的值为1表示互斥锁被线程持有。在内存创建互斥锁变量初始化为1。
- 由于exchange汇编指令是原子的,所以不管线程如何切换,只有一个线程能够将mutex(内存)中的1值交换到自己的寄存器当中,即该线程的上下文中。而线程上下文是线程的私有数据,实现了公有到私有的转换。同时寄存器当中的0值被交换到了mutex中,其他线程再进行交换也只能交换到0。
- 在进行if判断时,交换到1值的线程执行return 0,可以安全地进入临界区访问临界资源;而交换到0值的线程阻塞等待,直到互斥锁被解锁,这些线程才会被唤醒,然后再次尝试申请锁
- 当持有锁的线程访问完临界资源后,会将mutex变量重新置为1,即解锁互斥锁。之后OS唤醒等待互斥锁解锁的线程,让他们再次竞争申请锁。
- 为了保证锁的安全,申请和释放锁,必须是原子的!在设计加锁时,通过一条原子性的exchange指令,保证了加锁和解锁的原子性。
- 加锁了之后,线程在临界区中,是否会切换,会有问题吗?线程在临界区中也可能会被切换,但他是持有锁被切换的,所谓持有锁切换是指互斥锁的1值保存在当前线程的上下文,被当前线程私有。而其他线程即使被CPU调度执行,也无法抢占互斥锁,也就无法访问临界区代码。所以不会有任何问题。
假设存在的情况:
A执行1:cpu寄存器值=0;执行2前被切换;A带着CPU寄存器的值0回老家【保存上下文】
B执行1:cpu寄存器值=0;执行2,交换使得mutex=0,寄存器=1;执行3,if条件满足,进入if,执行4前被切换;B带着CPU寄存器的值1回老家【保存上下文】
A继续执行2,交换使得mutex=0,寄存器=1;执行3,if条件不满足,挂起等待。
B继续执行4,return 0;表示加锁成功。
A等待结束,执行6,进行A的加锁。
如此,mutex自己保证了自己的原子性。那个“1”就好像一个令牌,不管有多少个线程,令牌只有一个,线程即使被切换,他也是带着令牌走的。
有同学会问,我们为了使得一个全局变量ticket被安全的访问,又添加了一个mutex,现如今为了保护ticket搞了一个也要被保护的mutex,为什么不直接将保护mutex的思想用到ticket上?这是多此一举吗?
当然不是。在多线程编程中,多个地方都会用到锁,如果我们在项目中编写代码时,加一个锁就把对应的代码添加一些if/else/exchange,这样的代码质量简直难评。设计者给我们设计了一个线程库,设计者考虑到线程安全的问题,设计了锁这样的概念,为的就是让程序员使用时,能够简单通过加锁/解锁这样的语句实现复杂的原子操作,类似于封装有益于提高代码可维护性可重用性的思想。
7.可重入VS线程安全
线程安全:
多个线程并发执行时,在没有锁保护的情况下访问了共享资源(如全局或静态变量,堆区数据等),会出现数据竞争从而导致数据冲突,数据不一致等线程安全问题。多个线程并发执行同一段代码时,不会出现不同的结果称之为线程安全。常见的如果对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现线程不安全的问题。
重入:
同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们
称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
常见的线程不安全的情况
- 不保护共享变量的函数
- 函数状态随着被调用而发生改变的函数【使用count++】
- 返回指向静态变量指针的函数
- 调用线程不安全函数的函数
- AB线程分别执行a函数和b函数,ab函数都改变了ticket的值,也有可能导致线程不安去。
常见的线程安全的情况
- 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
- 类或者接口对于线程来说都是原子操作
- 多个线程之间的切换不会导致该接口的执行结果存在二义性
- 仅使用本地(局部)数据,或者通过制作全局数据的本地拷贝来保护全局数据。
- 使用互斥锁(Mutex)来保护对共享资源的访问。互斥锁可以确保在同一时间只有一个线程能够访问临界区,从而避免数据竞争的发生。
- 不调用线程不安全的函数
常见不可重入的情况
- 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
- 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
- 可重入函数体内使用了静态的数据结构
常见可重入的情况
- 不使用全局变量或静态变量
- 不使用用malloc或者new开辟出的空间
- 不调用不可重入函数
- 不返回静态或全局数据,所有数据都有函数的调用者提供
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据【在临界区前,搞一个局部变量存储可能会被影响的全局变量,临界区后,再把该值拷贝回去,保证此全局变量不会被改变】
可重入与线程安全联系
函数可重入的 ⇒ 线程安全
函数不可重入 ⇒ 不能由多个线程使用 ⇒ 有可能引发线程安全问题
一个函数中有全局变量 ⇒ 这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别
- 可重入函数是线程安全函数的一种
- 可重入函数 ⇒ 线程安全,线程安全不一定是可重入的
- 如果对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数锁还未释放则会产生死锁,因此是不可重入的【示例如下,即线程只使用一个锁的死锁现象】
8.完善后的代码
#include <iostream>
#include <thread>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <ctime>
#include <cassert>
#include <cstdio>
using namespace std;
// pthread\_mutex\_t 是原生线程库提供的一个数据类型
// pthread\_mutex\_t mtx = PTHREAD\_MUTEX\_INITIALIZER; 全局锁的初始化方式
/\*
#define PTHREAD\_MUTEX\_INITIALIZER
{
{
0, 0, 0, 0, 0, \_\_PTHREAD\_SPINS, { 0, 0 }
}
}
\*/
class ThreadData
{
public:
ThreadData(const std::string &threadName, pthread\_mutex\_t \*pmtx)
: \_threadName(threadName), \_pmtx(pmtx)
{
}
public:
std::string _threadName;
pthread\_mutex\_t \*_pmtx;
};
int tickets = 10000; // 并发访问如果不加保护的访问临界资源就会出现数据不一致
void \*getTickets(void \*args)
{
// 加锁的粒度越小越好
ThreadData \*td = (ThreadData \*)args;
int n = 0;
while (true)
{
// 抢票逻辑
n = pthread\_mutex\_lock(td->_pmtx);
assert(n == 0);
// 临界区
if (tickets > 0) // 1. 逻辑运算
{
// 模拟抢票业务
usleep(rand() % 1500);
tickets--; // 2. 算术运算
cout << td->_threadName << "have got one, remain: " << tickets << endl;
n = pthread\_mutex\_unlock(td->_pmtx);
assert(n == 0);
}
else
{
n = pthread\_mutex\_unlock(td->_pmtx);
assert(n == 0);
break;
}
// 抢完票后续动作
usleep(rand() % 2000);
}
delete td;
return nullptr;
}
#define THREAD\_NUM 100
int main()
{
time\_t start = time(nullptr);
srand((unsigned long)time(nullptr) ^ getpid() ^ 0x147);
pthread\_mutex\_t mtx;
pthread\_mutex\_init(&mtx, nullptr);
pthread\_t t[THREAD_NUM];
// 多线程抢票的逻辑
for (int i = 0; i < THREAD_NUM; i++)
{
std::string name = "thread " + std::to\_string(i + 1);
ThreadData \*td = new ThreadData(name, &mtx);
pthread\_create(t + i, nullptr, getTickets, (void \*)td);
}
for (int i = 0; i < THREAD_NUM; i++)
{
pthread\_join(t[i], nullptr);
}
pthread\_mutex\_destroy(&mtx);
time\_t end = time(nullptr); // typedef long time\_t
std::cout << "run time: " << (long long)(end - start) << "s" << std::endl;
}
设两个进程共用一个临界资源的互斥信号量mutex,当mutex=1时表示
没有一个进程进入临界区而非两个进程都在等待
mutex简单理解就是一个0/1的计数器,用于标记资源访问状态:
0表示已经有执行流加锁成功,资源处于不可访问
1表示没有执行流完成加锁,资源可访问
请简述什么是线程互斥,为什么需要互斥
线程互斥:线程互斥是一种机制,这种机制确保当多个线程会访问临界资源时只有一个线程能够访问临界资源,其他线程必须等待。
原因:如果没有互斥机制,会导致数据不一致或数据损坏。具体来说,多个线程可能在进程的地址空间内部同时运行,进程的大部分资源对于线程来说是共享的。当多个线程同时尝试对临界资源进行操作时,如果没有互斥机制,会导致数据不一致或数据损坏。
进程/线程信息
ps命令用于查看进程信息,其中-L选项用于查看轻量级进程信息
pthread_self() 用于获取用户态线程的tid,而并非轻量级进程ID
getpid() 用于获取当前进程的id,而并非某个特定的轻量级进程
在有多个线程的情况下,主线程调用pthread_cancel(pthread_self()), 则主线程状态为Z, 其他线程正常运行
主线程调用pthread_cancel(pthread_self())函数来退出自己, 则主线程对应的轻量级进程状态变更成为Z, 其他线程不受影响,这是正确的(正常情况下我们不会这么做)
在有多个线程的情况下,主线程从main函数的return返回或者调用pthread_exit函数,则整个进程退出
主线程调用pthread_exit只是退出主线程,并不会导致进程的退出
简述什么是LWP
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Linux运维工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Linux运维全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Linux运维知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加VX:vip1024b (备注Linux运维获取)
最后的话
最近很多小伙伴找我要Linux学习资料,于是我翻箱倒柜,整理了一些优质资源,涵盖视频、电子书、PPT等共享给大家!
资料预览
给大家整理的视频资料:
给大家整理的电子书资料:
如果本文对你有帮助,欢迎点赞、收藏、转发给朋友,让我有持续创作的动力!
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
最后的话
最近很多小伙伴找我要Linux学习资料,于是我翻箱倒柜,整理了一些优质资源,涵盖视频、电子书、PPT等共享给大家!
资料预览
给大家整理的视频资料:
[外链图片转存中…(img-xDE4fB5y-1712587683568)]
给大家整理的电子书资料:
[外链图片转存中…(img-bJo6rGTb-1712587683569)]
如果本文对你有帮助,欢迎点赞、收藏、转发给朋友,让我有持续创作的动力!
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-g9Kz0RNl-1712587683569)]