[Linux] 线程互斥分析: 多线程的问题、互斥锁、C++封装使用互斥锁、线程安全分析、死锁分析...
线程互斥
临界资源和临界区
- 临界资源: 不同执行流都可以看到的同一资源, 就叫做临界资源
- 临界区: 访问临界资源的代码, 就叫就临界区
- 原子性: 一个操作, 如果只存在两种状态: 未完成、已完成, 而没有中间状态, 就称这个操作是具有原子性的.
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using std::cout;
using std::endl;
int tickets = 10000;
// 查票
void* inqureTicket(void* args) {
const char* name = static_cast<const char*>(args);
int cnt = 10;
while (cnt--) {
if (tickets > 0) {
usleep(100000);
printf("%s: %lu 查到剩余票了, 还有: %d\n",name, pthread_self(), tickets);
usleep(100000);
}
else {
printf("没有票了\n", name);
break;
}
}
return nullptr;
}
int main() {
pthread_t tid1, tid2, tid3, tid4;
pthread_create(&tid1, nullptr, inqureTicket, (void*)"thread_1");
pthread_create(&tid2, nullptr, inqureTicket, (void*)"thread_2");
pthread_create(&tid3, nullptr, inqureTicket, (void*)"thread_3");
pthread_create(&tid4, nullptr, inqureTicket, (void*)"thread_4");
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
pthread_join(tid3, nullptr);
pthread_join(tid4, nullptr);
return 0;
}
tickets
) 可以被所有线程看到, 并访问. 所以 票即为临界资源
.inquireTicket()
对临界资源进行了访问, 那么 整个回调函数就都是临界区吗?if (tickets > 0)
访问临界资源, 对临界资源进行了判断.tickets
也对临界资源进行了访问.这一部分才叫 临界区
.多线程访问临界资源的问题
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using std::cout;
using std::endl;
int tickets = 10000;
void* grabTicket(void* args) {
const char* name = static_cast<const char*>(args);
while (true) {
if (tickets > 0) {
usleep(100);
printf("%s: %lu 抢到票了, 编号为: %d\n", name, pthread_self(), tickets--);
usleep(100);
}
else {
printf("没有票了, %s: %lu 放弃抢票\n", name, pthread_self());
break;
}
}
return nullptr;
}
int main() {
pthread_t tid1, tid2, tid3, tid4;
pthread_create(&tid1, nullptr, grabTicket, (void*)"thread_1");
pthread_create(&tid2, nullptr, grabTicket, (void*)"thread_2");
pthread_create(&tid3, nullptr, grabTicket, (void*)"thread_3");
pthread_create(&tid4, nullptr, grabTicket, (void*)"thread_4");
pthread_join(tid1, nullptr);
cout << "main thread join thread_1" << endl;
pthread_join(tid2, nullptr);
cout << "main thread join thread_2" << endl;
pthread_join(tid3, nullptr);
cout << "main thread join thread_3" << endl;
pthread_join(tid4, nullptr);
cout << "main thread join thread_4" << endl;
return 0;
}
tickets > 0
才会输出 抢到票了, 这句话.tickets > 0
和 tickets--
tickets > 0
计算时发生了一些错误.代码中有关算数计算的问题, 都是交给CPU执行的.
无论是
+ - * /
还是逻辑运算
还是逻辑判断
. 最终,CPU 都会通过
位移运算
和加法运算
来解决
- CPU读取判断并放入寄存器中
- CPU执行数据判断
- CPU将判断结果返回到代码中
tickets 为 1
, 且 线程1
进行判断时, 正常的情况是这样的:CPU 进行逻辑判断, 其实是通过 判断式子, 计算出一个真值或假值, 进而返回到 判断语句中.
例如, 此例中 tickets = 1, 判断
tickets > 0
, CPU 就可能 计算1 + -0
的结果, 然后将结果返回到 判断语句中
tickets--
.tickets = 1
时, 线程 1 需要进行判断, 但是 CPU计算完成之后, 还没有将结果返回给线程1的代码中, 却需要调度线程2了.tickets 依旧为1
, 然后 CPU 根据 tickets 为1进行计算, 计算完成之后, 还没有将结果返回给线程2代码中, 又需要调度线程3了.tickets 还是1
, 然后CPU 根据tickets为1进行计算, 计算完成之后, 正常将结果返回给了线程3的代码中, 此时 tickets为1, 所以 判断结果肯定为真, 所以线程3 执行抢票操作 tickets--
, tickets 变为 0
. 线程3抢到 编号为1
的票:tickets--
, tickets 变为 -1
. 线程1抢到 编号为0
的票:tickets--
, tickets 变为 -2
. 线程2 抢到 编号为-1
的票.线程2的再次调度, 与线程1再次调度的步骤相似, 不再画图演示
而 如果是执行
tickets--
时发生这种情况, 就有可能对票的数量修改混乱很可能出现这样的情况:
线程1 执行完
tickets--
结果是 9999, 但是需要调度其他线程, 所以需要将 9999 存储到线程1的上下文数据中.结果, CPU一直在调度其他线程, 票数实际已经被抢到了 5622. 但此时, 如果继续回去调度线程1, CPU就不会再计算, 而是恢复线程1的上下文数据, 然后发现已经计算过了, 就会直接将 9999 返回到线程1的代码中, 就会对全局变量作出修改. 将 5622 改为 9999.
此时 票的数量就会发生混乱.
-
无论
tickets > 0
还是tickets--
这两个计算操作都不是原子的
这两个操作都具有中间状态, 即 CPU计算的过程需要读取、计算、返回多个操作. 存在中间状态, 就有可能在处于中间状态的时候 暂停 然后其他线程访问同一个数据.
如果, 这两个操作都是原子性的, 根本不存在什么中间状态, 就不会再造成这种情况
-
在已经有一个线程访问临界资源的时候, 其他线程依旧可以访问临界资源.
mutex互斥量
- 代码必须要有互斥行为:当代码
进入临界区
执行时,不允许其他线程进入该临界区
- 如果多个线程同时要执行临界区的代码,并且临界区没有线程在执行,那么
只能允许一个线程进入该临界区
- 如果线程
不在临界区中
执行,那么该线程不能阻止其他线程进入临界区
核心宗旨
只有一个, 即 给临界区加一把锁
!互斥锁
. 给代码实现互斥效果锁的接口及使用 *
定义一个锁(造锁):
pthread_mutex_t mutex;
来定义一个互斥锁. 当然, 锁名可以随便设置.互斥量
互斥锁的类型
pthread_mutex_t
是一个联合体.
初始化锁(改锁):
pthread_mutex_init()
是 pthread 库提供的初始化锁的接口, 第一个参数传入的就是需要初始化的锁的地址.摧毁锁:
pthread_mutex_destroy()
用来摧毁定义的锁, 传入锁的指针.上锁:
pthread
库为用户提供了, 两种不同的上锁方式:pthread_mutex_lock()
, 阻塞式上锁. 即 线程执行此接口时, 指定的锁已经被锁上了, 那么线程就进入阻塞状态, 知道解锁之后 此线程再上锁.pthread_mutex_trylock()
, 非阻塞式上锁. 即 线程执行此接口时, 尝试上锁, 如果指定的锁已经被锁上, 那么线程就先不上锁, 先去执行其他代码.一般用于 进入临界区之前
解锁:
pthread_mutex_unlock()
解锁接口, 一般用于出临界区的时候
pthread
库提供的互斥锁, 以及锁的一些接口.#include <iostream>
#include <unistd.h>
#include <pthread.h>
using std::cout;
using std::endl;
int tickets = 10000;
pthread_mutex_t mutex; // 定义锁
void* grabTicket(void* args) {
const char* name = static_cast<const char*>(args);
while (true) {
pthread_mutex_lock(&mutex); // 在即将进入临界区时加锁
if (tickets > 0) {
usleep(100);
printf("%s: %lu 抢到票了, 编号为: %d\n", name, pthread_self(), tickets--);
usleep(100);
pthread_mutex_unlock(&mutex); // 在即将离开临界区时解锁
}
else {
printf("没有票了, %s: %lu 放弃抢票\n", name, pthread_self());
break;
}
}
return nullptr;
}
int main() {
pthread_mutex_init(&mutex, nullptr); // 初始化锁
pthread_t tid1, tid2, tid3, tid4;
pthread_create(&tid1, nullptr, grabTicket, (void*)"thread_1");
pthread_create(&tid2, nullptr, grabTicket, (void*)"thread_2");
pthread_create(&tid3, nullptr, grabTicket, (void*)"thread_3");
pthread_create(&tid4, nullptr, grabTicket, (void*)"thread_4");
pthread_join(tid1, nullptr);
cout << "main thread join thread_1" << endl;
pthread_join(tid2, nullptr);
cout << "main thread join thread_2" << endl;
pthread_join(tid3, nullptr);
cout << "main thread join thread_3" << endl;
pthread_join(tid4, nullptr);
cout << "main thread join thread_4" << endl;
return 0;
}
上锁和解锁, 一定要合理!
没有发生数据错误
的问题.整个进程卡住了
. 这是为什么?没有上锁
的时候, 整个代码是可以正常运行
的. 所以 一定是锁的问题
.最后一次执行判断
语句, 会进到 哪个控制块中呢?
是 if后 还是else后?在if后的控制块中解锁
了, 但是并 没有在else后的控制块中解锁
. 那么 最后一次票数判断之后, 就会直接退出线程
.没有对已经上了的锁进行解锁
.pthread_mutex_lock()
是阻塞式上锁的. 如果执行的时候, 指定的锁已经被锁上了, 那就会阻塞式等待, 线程就会暂停运行.上述现象是一种
死锁
现象.
死锁是指在多线程的运行时, 每个线程都在等待其他线程释放资源, 导致所有线程都无法继续执行的一种状态
(对进程也适用)
一个线程进入临界区加上锁之后, 其他进程就会进入阻塞状态. 在此线程的时间片内, 此线程就会一直进出临界区. 虽然此线程会在出临界区时解锁, 但是它又会马上进入下一个循环, 再次上锁.
其他线程想要申请到锁, 是需要先被CPU调度的, 线程的调度的消耗 对比 上锁和解锁消耗, 其实是很大的. 所以 线程调度并没有 上锁和解锁快
. 所以, 我们实现的代码 在申请到锁的线程的时间片内, 其他线程是很难抢到锁的
.线程解锁之后, 让线程等一会
不让他马上进入下一个循环. 让CPU有充足的时间调度其他线程
. 然后就可以看到 "百线争鸣"
啦锁的宏初始化
pthread
库 为 锁的初始化提供了相应的接口pthread_mutex_init()
.PTHREAD_MUTEX_INITIALIZER
.pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
或
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_destroy()
接口.#include <iostream>
#include <unistd.h>
#include <pthread.h>
using std::cout;
using std::endl;
int tickets = 10000;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 定义全局锁, 并用宏来初始化
void* grabTicket(void* args) {
const char* name = static_cast<const char*>(args);
while (true) {
pthread_mutex_lock(&mutex); // 在即将进入临界区时加锁
if (tickets > 0) {
printf("%s: %lu 抢到票了, 编号为: %d\n", name, pthread_self(), tickets--);
pthread_mutex_unlock(&mutex); // 在即将离开临界区时解锁
usleep(10);
}
else {
printf("没有票了, %s: %lu 放弃抢票\n", name, pthread_self());
pthread_mutex_unlock(&mutex); // 在线程即将退出时解锁
break;
}
}
return nullptr;
}
int main() {
pthread_t tid1, tid2, tid3, tid4;
pthread_create(&tid1, nullptr, grabTicket, (void*)"thread_1");
pthread_create(&tid2, nullptr, grabTicket, (void*)"thread_2");
pthread_create(&tid3, nullptr, grabTicket, (void*)"thread_3");
pthread_create(&tid4, nullptr, grabTicket, (void*)"thread_4");
pthread_join(tid1, nullptr);
cout << "main thread join thread_1" << endl;
pthread_join(tid2, nullptr);
cout << "main thread join thread_2" << endl;
pthread_join(tid3, nullptr);
cout << "main thread join thread_3" << endl;
pthread_join(tid4, nullptr);
cout << "main thread join thread_4" << endl;
return 0;
}
static
修饰的锁, 其实 线程执行的函数是看不到的.pthread_create()
的第四个参数 传入线程执行的函数中.#include <iostream>
#include <unistd.h>
#include <pthread.h>
using std::cout;
using std::endl;
int tickets = 10000;
void* grabTicket(void* args) {
pthread_mutex_t* pMutex = (pthread_mutex_t*)args; // 将传入的参数强转为 锁类型
while (true) {
pthread_mutex_lock(pMutex); // 在即将进入临界区时加锁
if (tickets > 0) {
printf("thread: %lu 抢到票了, 编号为: %d\n", pthread_self(), tickets--);
pthread_mutex_unlock(pMutex); // 在即将离开临界区时解锁
usleep(10);
}
else {
printf("没有票了, thread: %lu 放弃抢票\n", pthread_self());
pthread_mutex_unlock(pMutex); // 在线程即将退出时解锁
break;
}
}
return nullptr;
}
int main() {
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_t tid1, tid2, tid3, tid4;
pthread_create(&tid1, nullptr, grabTicket, (void*)&mutex); // 将锁地址当参数传入
pthread_create(&tid2, nullptr, grabTicket, (void*)&mutex);
pthread_create(&tid3, nullptr, grabTicket, (void*)&mutex);
pthread_create(&tid4, nullptr, grabTicket, (void*)&mutex);
pthread_join(tid1, nullptr);
cout << "main thread join thread_1" << endl;
pthread_join(tid2, nullptr);
cout << "main thread join thread_2" << endl;
pthread_join(tid3, nullptr);
cout << "main thread join thread_3" << endl;
pthread_join(tid4, nullptr);
cout << "main thread join thread_4" << endl;
return 0;
}
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
using std::cout;
using std::endl;
int tickets = 10000;
typedef struct threadData {
char _name[64];
pthread_mutex_t* _mutex;
}threadData; // 定义struct 成员包括 name 和 锁
void* grabTicket(void* args) {
threadData* tD = (threadData*)args;
while (true) {
pthread_mutex_lock(tD->_mutex); // 在即将进入临界区时加锁
if (tickets > 0) {
printf("%s: %lu 抢到票了, 编号为: %d\n", tD->_name, pthread_self(), tickets--);
pthread_mutex_unlock(tD->_mutex); // 在即将离开临界区时解锁
usleep(10);
}
else {
printf("没有票了, %s: %lu 放弃抢票\n", tD->_name, pthread_self());
pthread_mutex_unlock(tD->_mutex); // 在线程即将退出时解锁
break;
}
}
return nullptr;
}
int main() {
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_t tid1, tid2, tid3, tid4;
threadData* tD1 = new threadData();
threadData* tD2 = new threadData();
threadData* tD3 = new threadData();
threadData* tD4 = new threadData();
tD1->_mutex = &mutex;
tD2->_mutex = &mutex;
tD3->_mutex = &mutex;
tD4->_mutex = &mutex;
strcpy(tD1->_name, "thread_1");
strcpy(tD2->_name, "thread_2");
strcpy(tD3->_name, "thread_3");
strcpy(tD4->_name, "thread_4");
pthread_create(&tid1, nullptr, grabTicket, (void*)tD1);
pthread_create(&tid2, nullptr, grabTicket, (void*)tD2);
pthread_create(&tid3, nullptr, grabTicket, (void*)tD3);
pthread_create(&tid4, nullptr, grabTicket, (void*)tD4);
pthread_join(tid1, nullptr);
cout << "main thread join thread_1" << endl;
pthread_join(tid2, nullptr);
cout << "main thread join thread_2" << endl;
pthread_join(tid3, nullptr);
cout << "main thread join thread_3" << endl;
pthread_join(tid4, nullptr);
cout << "main thread join thread_4" << endl;
return 0;
}
锁的作用
如果线程对临界区上了锁, 还没有出临界区, 但是此时需要调用其他线程了, 当前线程会被切走吗? 其他线程可以进入临界区吗?还会不会对临界资源有一定的影响?
会被切走
, 但是 其他线程不可能再进入临界区
, 就不可能再访问临界资源
.可以被其他所有线程看到的、只有一个的
**变量.申请到锁、并且还没有解锁的线程当前并没有被调度, 其他线程也无法申请锁.
**申请到锁, 就是把锁拿走了
. 没有解锁
的时候, 就一直拿着锁
. 即使没有被调度, 此线程也一直拿着锁. 其他线程就无法申请到锁
. 无法申请到锁, 就无法继续执行代码
.所以, 尽量不要在加了锁的临界区内做非常耗时的事情.
锁
不就是一个临界资源
吗?互斥锁的上锁和解锁过程, 已经被设计为了原子的
锁的大概原理
pthread_mutex_lock()
) 和 解锁(pthread_mutex_unlock()
) 这两个过程, 是值得介绍以下的.swap
或 exchange
等指令. 此指令的作用是, 直接将寄存器中的数据与内存中的数据做交换. 只有一条指令, 此指令是原子的.// lock 伪代码
movb $0, %al
xchgb %al, mutex
if(al > 0) {
return 0;
}
else
阻塞等待;
goto lock;
al
表示寄存器, mutex
则表示在内存中的锁movb $0, %al
, 把 0 存入 al 寄存器中xchgb %al, mutex
, 交换 al寄存器 和 内存中mutex 的数据if(al > 0) { return 0; }
, 如果 al 寄存器中的数据 大于 0, 则 申请锁成功, 返回 0.xchgb %al, mutex
操作 是实际上锁的操作.xchgb %al, mutex
将 al 中的0 与 mutex 的值交换, 其实就是 将 锁给了执行此语句的线程
线程的属性中是有维护寄存器的数据的, 即 线程的上下文数据
.al寄存器中的数据
, 其实就是 线程的上下文数据
. 如果没有解锁
, 那么 此线程从CPU 切走
时, 会将 寄存器数据(上下文数据)维护好, 一起切走
. 而此时寄存器中的数据可能没有被清除
, 因为可能只是将寄存器数据存储到线程上下文数据中既然寄存器中数据还有
, 那么下一个线程被调度
的时候, 不会直接读取寄存器中的数据吗?
不会
, 新的线程被调度的时候, 首先
要做的就是将线程自己的有关寄存器上下文数据恢复到寄存器中
. 也就是说, 当新线程被调度之后, CPU寄存器中会变为此线程的上下文数据.xchgb %al, mutex
就是将锁给了执行此语句的线程.将内存中的数据换入寄存器中, 本质上就是 将内存中的数据 从共享状态 变为了线程私有
. 因为 换入寄存器的数据, 基本当前线程的上下文数据.C++封装互斥锁及相关使用 *
threadLock.hpp:
#pragma once
#include <iostream>
#include <pthread.h>
using std::cout;
using std::endl;
class myMutex {
public:
myMutex() {
pthread_mutex_init(&_lock, nullptr);
}
void lock() {
pthread_mutex_lock(&_lock);
}
void unlock() {
pthread_mutex_unlock(&_lock);
}
~myMutex() {
pthread_mutex_destroy(&_lock);
}
private:
pthread_mutex_t _lock;
};
// 锁—警卫
class lockGuard {
public:
lockGuard(myMutex* myMutex)
: _myMutex(myMutex) {
_myMutex->lock();
printf("上锁成功……\n");
}
~lockGuard() {
_myMutex->unlock();
printf("解锁成功……\n");
}
private:
myMutex* _myMutex;
};
myThread.cpp:
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include "threadLock.hpp"
int tickets = 10000;
myMutex mymutex; // 定义一个锁类
// 将抢票操作, 独立为一个函数实现
// 抢票函数内有临界区, 所以需要上锁
bool grabTickets() {
bool ret = false; // 定义一个变量用于返回, 默认为false, 抢票成功改为 true
lockGuard guard(&mymutex); // 上锁!
if (tickets > 0) {
printf("thread: %lu 抢到票了, 编号为: %d\n", pthread_self(), tickets--);
ret = true;
usleep(100);
}
return ret;
}
// 这个才是线程需要执行的函数
void* startRoutine(void* args) {
const char* name = static_cast<const char*>(args); // 强转
while (true) {
if (!grabTickets()) {
// 如果抢票失败, 就 退出循环
break;
}
printf("%s, grab tickets success\n", name);
sleep(1); // 为了方便观察, 设置为1s
}
return nullptr;
}
int main() {
pthread_t tid1, tid2, tid3, tid4;
pthread_create(&tid1, nullptr, startRoutine, (void*)"thread_1");
pthread_create(&tid2, nullptr, startRoutine, (void*)"thread_2");
pthread_create(&tid3, nullptr, startRoutine, (void*)"thread_3");
pthread_create(&tid4, nullptr, startRoutine, (void*)"thread_4");
pthread_join(tid1, nullptr);
printf("main thread join thread_1\n");
pthread_join(tid2, nullptr);
printf("main thread join thread_2\n");
pthread_join(tid3, nullptr);
printf("main thread join thread_3\n");
pthread_join(tid4, nullptr);
printf("main thread join thread_4\n");
return 0;
}
threadLock.hpp
的这段代码中, 我们封装了两个类:-
myMutex互斥量类
, 即锁类
.构造函数的内容就是 锁的初始化. 析构函数的内容就是 锁的摧毁.
还有两个成员函数就是 上锁和解锁.
-
lockGuard类
, 此类其实是为了更加简单的使用上锁和解锁而封装的.此类中, 定义了一个成员变量 是我们封装的 myMutex类.
而 此类的构造函数, 首先通过初始化列表初始化成员变量. 然后通过成员变量来调用我们封装过的上锁函数. 就可以达到一个 实例化
lockGuard
对象自动上锁的功能然后是 此类的析构函数, 析构函数的内容就是通过成员变量调用我们封装过的解锁函数. 就可以达到 在
lockGuard
对象析构的时候, 自动解锁的功能
myThread.cpp
的这段使用我们封装的类的代码中, 我们是怎么上锁和解锁的?myMutex 对象
, 以便于线程可以看到.myMutex 对象
实例化了一个 lockGuard 对象
. 因为, 实例化 对象会自动执行构造函数, 而 lockGuard 类的构造函数内容就是 上锁
. 所以 实例化 lockGuard 对象
就是自动上锁了.lockGuard
类的析构函数内容就是 解锁
. 所以 lockGuard 对象
生命结束, 就是自动解锁了.我们实现的上锁和解锁功能, 是基于C++类的特性来实现的.
并且, 一个类的生命周期是在其所在的控制块内. 所以还可以这样使用:
#include <iostream> #include "threadLock.hpp" int cnt = 0; myMutex mymutex; void* startRoutine(void* args) { // 如果我们需要统计线程执行此函数了多少次, 我们只需要使用下面这段代码块 { lockGuard myLock(&mymutex); cnt++; } // 这也是一个控制块, myLock对象 出此控制块会自动调用析构函数, 即出此控制块会自动解锁. // …… 其他代码 }
可重入 VS 线程安全
概念
-
线程安全:
多线程并发运行同一段代码时, 单一线程不会影响到其他线程或整个进程的运行结果, 就叫做线程安全
如果会影响线程或进程, 就被称为
线程不安全
-
可重入:
同一个函数被不同执行流调用, 在一个执行流执行没结束时, 有其他执行流再次执行此函数, 这个现象叫重入
.如果,
函数被重入执行结果等不会发生错误, 则成此函数为可重入的函数
. 否则 为不可重入的函数函数被重入会出现错误, 其实是因为代码编写错误出现了BUG. 其实是编写者的问题, 而不是函数的问题.
重入是一种特性, 而不是一种错误.
常见的线程不安全的情况
-
不保护共享变量的函数
-
函数状态随着被调用 会发生变化的函数
比如, 我们在函数内部定义了一个静态变量, 然后不加锁的++, 用来统计线程调用此函数的次数
-
返回指向静态变量指针的函数
-
调用线程不安全函数的函数
常见的线程安全的情况
- 每个线程对全局变量或者静态变量
只有读取权限,没有写入权限
,一般来说这些线程是安全
的 - 类或接口对于线程来说都是
原子操作
- 多个线程之间的切换
不会导致
该接口的执行结果存在二义性
常见不可重入的情况 **
-
调用了malloc/free
函数,因为malloc函数是用全局链表来管理堆的
如何理解
malloc
使用全局链表管理堆的呢?其实, malloc 在堆区动态开辟空间, 实际是调用了 系统调用brk. 并且 并不只是简单的开辟一块空间. 还需要将开辟出的空间以 全局链表的形式管理起来.
进程地址空间是由PCB维护的, 在源码中即 task_struct 的成员 mm_struct 类型的变量.
而在mm_struct 结构体中, 存在着一个成员是用来描述 虚拟内存列表:
此变量就是用来维护开辟出来的堆的:
我们使用 malloc 开辟出10块空间, 就会以 vm 的形式组成一个 10个节点的链表. 释放一块空间, 就会删除一个节点.
这就是管理堆的形式. 此链表是全局的.
-
调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
-
可重入函数体内使用了静态的数据结构
常见可重入的情况
- 不使用全局变量或静态变量
- 不使用用malloc或者new开辟出的空间
- 不调用不可重入函数
- 不返回静态或全局数据,所有数据都有函数的调用者提供
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
可重入与线程安全联系
- 函数是可重入的,那就是线程安全的
- 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的
可重入与线程安全区别
- 重入函数是线程安全函数的一种
- 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的. 但如果这个重入函数 若锁还未释放则可能会产生死锁,因此是不可重入的
死锁
. 那什么是死锁?死锁
死锁
是什么?死锁
:在一组进程、线程中的各个进程、线程 均占有不会释放的资源
,但 因互相申请被其他进程、线程所占用不会释放的资源而处于的一种永久等待状态
.#include <iostream>
#include <unistd.h>
#include <pthread.h>
int cnt = 0;
pthread_mutex_t mutexA = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutexB = PTHREAD_MUTEX_INITIALIZER;
void* startRoutineA(void* args) {
while (true) {
pthread_mutex_lock(&mutexA);
sleep(1);
pthread_mutex_lock(&mutexB);
cnt++;
printf("cnt: %d", cnt);
pthread_mutex_unlock(&mutexA);
pthread_mutex_unlock(&mutexB);
}
return nullptr;
}
void* startRoutineB(void* args) {
while (true) {
pthread_mutex_lock(&mutexB);
sleep(1);
pthread_mutex_lock(&mutexA);
cnt++;
printf("cnt: %d", cnt);
pthread_mutex_unlock(&mutexB);
pthread_mutex_unlock(&mutexA);
}
return nullptr;
}
int main() {
pthread_mutex_init(&mutexA, nullptr);
pthread_mutex_init(&mutexB, nullptr);
pthread_t tidA, tidB;
pthread_create(&tidA, nullptr, startRoutineA, (void*)"threadA");
pthread_create(&tidB, nullptr, startRoutineB, (void*)"threadB");
pthread_join(tidA, nullptr);
pthread_join(tidB, nullptr);
return 0;
}
几乎
是同时的.线程A 拿着锁A, 在申请锁B
, 线程B 拿着锁B, 在申请锁A
. 他俩都申请不到
, 直接造成死锁
.死锁产生的必要条件
- 互斥条件:: 一个资源每次只能被一个执行流使用
- 请求与保持条件: 一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件: 一个执行流已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件: 若干执行流之间形成一种头尾相接的循环等待资源的关系
死锁的避免方法
不使用锁
. 虽然锁可以解决一些多线程的问题, 但是可能会造成死锁, 而且 上锁和解锁的过程是需要消耗资源的. 如果不停的上锁和解锁, 一定会托慢进程的运行速度.最好先不要考虑如何设置锁, 可以先考虑一下是否可以不用锁
- 破坏死锁的四个必要条件
- 加锁顺序一致
- 避免锁未释放的场景
- 资源一次性分配
作者: 哈米d1ch 发表日期:2023 年 4 月 16 日