线程互斥(Thread Mutex)是指在多线程并发执行的程序中,为了避免多个线程同时访问临界区(Critical Section)而采用的一种同步机制。临界区是指对共享资源进行访问的代码段(也可以参考上面的概念),例如对共享变量的读写操作等。
线程互斥的目的是保证同一时刻只有一个线程能够进入临界区,以防止多个线程同时对共享资源进行修改,导致数据不一致或者结果出错。一般情况下,采用**互斥锁(Mutex,也称为互斥量)实现线程互斥,也可以使用其他同步机制,如信号量(Semaphore)、条件变量(Condition Variable)**等。
基本概念总结:
- 临界资源:多线程执行流共享的资源就叫做临界资源。
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
- 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。
下面我们就分别来学习互斥锁(也称为互斥量,为了统一,下面我们都称为互斥锁!)、条件变量、信号量和一些常见的锁情况!
Ⅱ. 互斥锁的概念
互斥锁(mutex)是一种基本的线程同步机制,用于解决多个线程访问共享资源时可能发生的竞态条件问题。它可以保证在同一时刻只有一个线程能够获取锁,并进入临界区。当一个线程获取到互斥锁时,其他线程尝试获取该锁时会被阻塞,直到该线程释放锁。
互斥锁提供了两个基本操作:加锁 && 解锁
- 加锁操作用于获取锁,防止其他线程进入临界区。
- 解锁操作用于释放锁,允许其他线程获取锁并进入临界区。
需要注意的是,在使用互斥锁时,应该注意死锁问题。 死锁是指两个或多个线程相互等待对方释放锁,导致程序陷入无限等待的状态。为了避免死锁问题,应该避免在临界区内调用阻塞操作(如等待信号量、等待条件变量等),并且应该尽可能减小临界区的范围,以避免锁竞争问题。
在 C++ 中,互斥量可以使用标准库中的
std::mutex类来实现。std::mutex类提供了两个基本操作:lock()和unlock(),分别用于获取锁定状态和释放锁定状态。当一个线程获取了锁定状态后,其他线程将不能再获取锁定状态,直到该线程释放锁定状态。 具体的内容请翻阅 C++ 笔记的讲解!
下面我们看一段代码,就能理解为何需要有互斥锁(下面的 thread.hpp 头文件是线程控制笔记中封装的线程库,可以去翻阅,这里直接调用):
#include "thread.hpp"
#include <memory>
#include <unistd.h>
int ticket = 10; // 全局变量
void* thread_routine(void* args)
{
string name = static_cast<const char*>(args);
while(true)
{
// 票数大于0则抢票,否则退出
if(ticket > 0)
{
usleep(12345); // 1秒=10^3毫秒=10^6微秒
cout << "i am new thread, name: " << name << ",the ticket: " << ticket << endl;
ticket--;
}
else
break;
}
}
int main()
{
unique_ptr<Thread> t1(new Thread(thread_routine, (void*)"user1", 1));
unique_ptr<Thread> t2(new Thread(thread_routine, (void*)"user2", 2));
unique_ptr<Thread> t3(new Thread(thread_routine, (void*)"user3", 3));
unique_ptr<Thread> t4(new Thread(thread_routine, (void*)"user4", 4));
unique_ptr<Thread> t5(new Thread(thread_routine, (void*)"user5", 5));
t1->join();
t2->join();
t3->join();
t4->join();
t5->join();
return 0;
}
上述代码是一个模拟抢票的系统,就是多个线程同时对全局变量 ticket 进行减减操作,而 ticket 也就是我们上面所说的临界资源啦,既然多个线程对 ticket 同时进行操作,那么势必会造成一些竞争条件问题,那么到底是怎么发生这个问题的呢❓❓❓
其实就是因为可能会发生这种情况:当多个线程同时来到 if(ticket > 0) 语句进行判断的时候,此时如果 ticket 已经是 1 了,那么此时假设 线程A 进入了这个语句,但是刚进入的时候就发生了一些情况比如说时间片切换、**CPU**突然调度一个优先级更高的线程导致该 线程A 等待的情况,这个时候还没将 ticket 进行减减操作的时候,轮到了 线程B 来被调度了,线程B 判断此时 if(ticket > 0) 依然是成立的,就直接进入了代码块中,相同情况,可能此时有更多的线程同时完成上面的操作,也就是说这个 if(ticket > 0) 就是一个摆设,然后这多个线程最后在 ticket 为 1 的时候直接进行了多次的减减操作,就导致了最后这种情况!除此之外,--操作本身就不是具有原子性的!因为其本质对应三条汇编指令。
而上面代码中的 usleep() 函数其实也是有问题的,它加大了出现这种情况发生的概率!如果把它去掉,可能程序不会出问题,但是数据量大的时候,依然会出毛病,所以并不是 usleep() 的问题,而是这个程序本身就有上述的两个问题!那为何 usleep() 能够加大出现这种情况发生的概率的呢❓❓❓
其实是因为 usleep() 函数会使得线程休眠一段时间,因此多个线程访问 ticket 变量的时序是不确定的,这会增加竞态条件的出现概率。
具体来说,usleep() 函数的作用是使线程休眠一段时间,这个时间是以微秒为单位的,可以看做是随机的。这就意味着,不同的线程在不同的时间点抢票,访问 ticket 变量的顺序是不确定的。如果多个线程同时访问 ticket 变量,就有可能导致竞态条件的出现,从而使程序出现错误。
在一个多线程程序中,当一个线程调用了
usleep()等休眠函数时,操作系统会将该线程的状态标记为 “阻塞”,并将CPU时间片分配给其他可以运行的线程。这样,就会出现线程切换的情况,其他线程就有机会运行。 在这个过程中,调用
usleep()函数的线程可能被放入等待队列中,等到休眠时间结束后,再次被调度执行。如果在此期间其他线程访问了共享变量,可能会导致竞态条件问题的出现。因此,为了避免这种情况,需要使用同步机制来保护共享变量的访问。
因为有了这种竞争条件问题,所以我们必须解决以上问题,而我们需要做到三点:
- 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 相反,如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
Ⅲ. 互斥锁的接口
一、互斥锁的定义
定义互斥锁对象的代码如下:
pthread_mutex_t lock; 其中 pthread_mutex_t 的类型定义如下:
/* Data structures for mutex handling. The structure of the attribute type is not exposed on purpose. */
typedef union
{
struct{
int __lock; // 锁的状态值,用于表示锁是否被占用。
unsigned int __count; // 锁的计数器,用于支持可重入锁。
int __owner; // 锁的拥有者线程ID。
/* KIND必须保持在结构中的这个位置,以保持二进制兼容性 */
int __kind; // 锁的类型,包括PTHREAD_MUTEX_NORMAL(普通锁)、PTHREAD_MUTEX_RECURSIVE(可重入锁)等。
unsigned int __nusers; // 锁的使用者数,用于支持可重入锁。
int __spins; // 自旋次数,用于支持PTHREAD_MUTEX_ADAPTIVE_NP(自旋锁)类型的锁。
} __data;
char __size[__SIZEOF_PTHREAD_MUTEX_T]; // 用于保证pthread_mutex_t的大小和对齐
long int __align; // 让该结构体对齐到long int类型的边界上
} pthread_mutex_t; 上述定义中,由于 pthread_mutex_t 结构体的大小是根据 __SIZEOF_PTHREAD_MUTEX_T 宏定义来设置的,因此 __size 数组的大小也就是根据该宏定义来确定的。
__align 是为了保证该结构体的对齐方式和 long int 类型一致,从而提高程序的性能。在某些架构上,如果结构体的成员变量没有被正确地对齐,就可能导致程序在运行时出现异常。因此,在定义结构体时,需要考虑到成员变量的对齐方式。
二、初始化互斥锁
初始化互斥锁其实有两种方法:静态分配、动态分配。
静态初始化:
#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 使用缺省的属性初始化这些互斥锁动态初始化:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
// 功能:动态初始化互斥锁
// 返回值:成功返回0,失败的话返回一个错误编号
// 参数:
// mutex:要初始化的互斥锁
// attr:互斥锁的属性,一般设置为nullptr即可三、销毁互斥锁
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 功能:销毁互斥锁
// 返回值:成功返回0,失败的话返回一个错误编号
// 参数:
// mutex:要销毁的互斥锁💥需要注意的事项:
- 使用
PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁。 - 不要销毁一个已经加锁的互斥量。
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。
四、互斥量的加锁和解锁
① 加锁接口
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 功能:对互斥锁加锁
// 返回值:成功返回0,失败的话返回一个错误编号(其中pthread_mutex_trylock只有在目标锁没有被占有的情况下才调用成功返回0)
// 参数:
// mutex:要加锁的互斥锁pthread_mutex_lock 和 pthread_mutex_trylock 都是用来加锁的函数,它们的区别在于:
pthread_mutex_lock函数会一直阻塞等待锁被释放,并且在获得锁之前线程会被挂起。如果锁一直被占用,那么这个线程会一直阻塞,导致死锁的问题。pthread_mutex_trylock函数会尝试加锁,如果锁已经被其他线程占用,则函数返回一个特定的错误码(EAGAIN或EBUSY,具体取决于实现),否则函数返回0。
因此,如果一个线程需要在获得锁之前继续执行其他操作,那么可以使用 pthread_mutex_trylock 函数。但是需要注意,使用 pthread_mutex_trylock 函数可能会造成线程的忙等待,从而浪费 CPU 资源,因此需要谨慎使用。而 pthread_mutex_lock 则是一个典型的阻塞函数,它会让线程进入睡眠状态,直到锁被释放才会被唤醒。
比如以下的情况(伪代码):
pthread_mutex_t mtx;
// 伪代码,假设多个线程同时进入
// 情况一:
void* fun(void* args)
{
pthread_mutex_lock(&mtx); // 不小心的多次加锁,这会导致死锁的问题
// ......
pthread_mutex_lock(&mtx);
}
// 情况二:
void* fun(void* args)
{
// 不小心的多次加锁,但是使用的pthread_mutex_trylock的话,会判断该锁是否被占有
// 是的话则不会加锁,会一直阻塞着,直到锁被释放,就比较安全,但是换来的是浪费CPU资源的代价!
pthread_mutex_trylock(&mtx);
// ......
pthread_mutex_lock(&mtx);
} 线程忙等待(
Busy Waiting)是一种线程等待的方式,它通常在一个循环中不断地检查某个条件是否满足,如果条件不满足,则继续等待。这种等待方式会占用CPU资源,降低系统的性能。 在线程互斥中,线程忙等待常常用于实现自旋锁(
Spin Lock),即在竞争不激烈的情况下,线程通过循环不断地尝试获取锁,以避免线程切换带来的开销。但是,如果线程竞争激烈,忙等待会导致大量的上下文切换,反而降低了性能。 为了避免忙等待带来的问题,可以使用睡眠等待(
Sleeping)的方式,即当线程无法获取锁时,释放CPU资源并让出时间片,等待其他线程释放锁后再去竞争锁。这样可以有效避免线程忙等待带来的问题。
② 解锁接口
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 功能:将互斥锁解锁
// 返回值:成功返回0,失败的话返回一个错误编号
// 参数:
// mutex:要解锁的互斥锁五、改进买票系统
之前遇到的问题现在我们就能解决了,就是当我们在进行模拟买票的时候进行加锁保护,保证每次只有一个线程能够访问这份临界资源进行操作,而其它的线程只能阻塞等待着:
#include "thread.hpp"
#include <memory>
#include <unistd.h>
int ticket = 10;
// 封装成类,这样子我们就能通过传类来让thread_routine看到字符消息和锁
class ThreadData
{
public:
ThreadData(const string& name, pthread_mutex_t* mtx)
:_name(name), _mtx(mtx)
{}
~ThreadData()
{}
public:
string _name;
pthread_mutex_t* _mtx;
};
void* thread_routine(void* args)
{
ThreadData* td = static_cast<ThreadData*>(args);
while(true)
{
pthread_mutex_lock(td->_mtx);
if(ticket > 0)
{
usleep(12345); // 1秒=10^3毫秒=10^6微秒
cout << "new thread -> name: " << td->_name << ",the ticket: " << ticket << endl;
ticket--;
pthread_mutex_unlock(td->_mtx); // 释放锁
}
else
{
pthread_mutex_unlock(td->_mtx); // 记得不满足也要释放锁,不然直接break会造成死锁
break;
}
// 为了避免某个线程长时间占用CPU资源,在每次售票后通过usleep函数让线程睡眠一段时间
// 其实这和我们生活上也是贴切的,买完票还需要执行其它工作,比如生成订单等等,所以抢完票之后需要等待一会
usleep(1000);
}
}
int main()
{
pthread_mutex_t mtx; // 定义一把锁
pthread_mutex_init(&mtx, nullptr); // 对锁进行初始化
// 创建一个临时的ThreadData传递过去,但是mtx是同一个mtx
unique_ptr<Thread> t1(new Thread(thread_routine, new ThreadData("user1", &mtx), 1));
unique_ptr<Thread> t2(new Thread(thread_routine, new ThreadData("user2", &mtx), 2));
unique_ptr<Thread> t3(new Thread(thread_routine, new ThreadData("user3", &mtx), 3));
unique_ptr<Thread> t4(new Thread(thread_routine, new ThreadData("user4", &mtx), 4));
unique_ptr<Thread> t5(new Thread(thread_routine, new ThreadData("user5", &mtx), 5));
t1->join();
t2->join();
t3->join();
t4->join();
t5->join();
pthread_mutex_destroy(&mtx); // 释放锁
return 0;
}
首先定义了一个全局变量 ticket 表示票的数量,然后定义了一个 ThreadData 类,包含了线程的名字和一个互斥锁。线程函数 thread_routine 在执行时会先对互斥锁进行加锁,然后判断当前票的数量是否大于 0,如果大于 0,则输出线程名字和剩余票数,并将票数减 1。最后释放互斥锁。
如果票数小于等于 0,则直接释放互斥锁,并退出循环。
为了避免某个线程长时间占用 CPU 资源,在每次售票后通过 usleep 函数让线程睡眠一段时间。这样可以模拟在真实场景中购票后需要执行其它操作的情况。
除此之外,其实锁的定义不只是上面这种方式(通过传递类的引用让多线程获取),我们还可以让锁用 static 修饰,或者设置为全局变量,这样子的话我们就可以不用通过传引用让线程函数获取,并且一般我们对这两种方式都是通过静态初始化锁,所以不用去释放它们!
💥注意事项
- 我们在 加锁的时候,一定要让临界区的区域尽量的小,也就是锁中间执行的代码要尽量的少。因为区域越大,一些没必要加锁的资源或者代码也被加锁了,而加锁导致的是让多个线程串行的访问,这势必会大大降低程序的效率。
- 要避免出现只给部分线程加锁的现象,这是及其不安全的,容易出
bug,所以要加锁,就得给全部线程都加上! - 对 没有持有锁的线程 来说,有意义的就只有两种状态:申请锁前、释放锁后!也就是说,站在 没有持有锁的线程 来看待持有锁的线程的执行过程,是原子性的!
- 对于 当前持有锁的线程 来说,如果在执行临界区代码的时候发生了线程切换等情况,那么锁还是当前线程持有的,并不会转移或者消失!
Ⅳ. 互斥锁的实现原理
一、问题引入
首先我们要明白的是,我们拿互斥锁去保护所谓的临界资源,那对于我们的互斥锁来说,它不也是一个被多个线程同时访问的临界资源吗❓❓❓
既然如此,互斥锁的实现也必须保证原子性,这样子才能保证线程安全,如果互斥锁自己都不安全了,谈何保护其它资源呢对吧。
当然,我们所使用的互斥锁肯定是原子性的,所以我们就要来一探究竟,看看它是如何实现原子性的呢!
二、复习知识
在讲互斥锁实现原理之前,我们必须来复习一下几个我们之前一直谈的共识,帮助我们理解下面的实现原理:
CPU内寄存器硬件是被所有执行流共享的。CPU内寄存器的内容是每个执行流所私有的,也就是每个执行流的上下文。- 当执行流切换走的时候,需要将其私有的上下文带走;而当执行流切换回来的时候,需要将其私有的上下文重新写入寄存器。
三、实现原理
首先我们要明白,实现互斥锁的方式有多种,不只是这里讲的这种原理,但是大多数体系结构中都是如此设计的,可以当作认识!
在之前我们也讲过,++ 和 -- 操作并不是原子性的,它们在底层汇编其实是三条语句,在执行其中任何一条的时候,如果发生了线程切换等情况,都会导致线程不安全的问题,所以我们加锁保证其原子性。
我们还说过原子性是一条汇编语句,其实这是不严谨,其实这只是原子性的一个子集,原子性指一个操作是不可分割的,即要么该操作全部完成,要么不完成,不会出现中间状态。在并发编程中,原子性操作是指一组操作要么全部执行,要么全部不执行,不会出现部分执行的情况。
为了实现互斥锁操作,大多数体系结构都提供了 swap 或 exchange 指令,该指令的作用是把寄存器和内存单元的数据直接交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
加锁的伪代码如下:
下面我们结合场景和伪代码来解释一下,假设当前有两个线程,分别是 线程A 和 线程B。一开始我们的锁 mutex 中的某个字段在内存中,存放的数据是 1,代表此时锁是空闲的。此时 线程A 来了( 线程B 也行,都一样的,这里假设是 线程A ),那么它就先执行 moveb $0, %al ,将 0 写入到 al寄存器 中,可能此时 线程A 会被切换,但是这并不重要,因为此时还没上锁,切换到别的线程的话,别的线程也还是会执行这步的!
真正关键的是这个第二步,也就是 xchgb %al, mutex ,假设现在还是轮到 线程A 在执行这句汇编语句,这一步就是将 线程A 在 al寄存器 中的数据,也就是此时的 0,与 mutex 在内存中的状态数据 1 进行交换,此时 mutex 的状态数据就变成了 0,而 线程A 在 al寄存器 中的数据就变成了 1。下面如果 线程A 继续执行的话,到达 if 语句了之后,直接判断此时 线程A 的 al寄存器 中的内容是 1,也就是大于零,那么就直接 return 0 了!
如下图所示**(下图中的 mutex 打错了,见谅!)**:

此时如果发生了线程切换,切换到了 线程B ,那么 线程B 也会执行 lock 函数中的这两条语句,首先还是一样执行 moveb $0, %al ,将 线程B 的 al寄存器 填入 0。关键点来了,执行第二句汇编语句的时候,也就是 xchgb %al, mutex ,此时 线程B 会发现 mutex 中的值已经不是 1 了,而是 0,但是它还是得照做,将 mutex 中的 0 与 线程B 在 al寄存器 中的 0 进行交换,此时两者都为 0。
接着 线程B 来到下面的 if 语句,判断到此时 al寄存器 中的内容为 0,则直接到 else 语句中执行挂起等待的操作!
这个挂起等待就是阻塞等待,直到这个锁被释放为止,才会离开 else 语句。随后 线程B 又会执行下面的 goto lock 伪代码,注意这里是伪代码,不是真的 goto 语句,这个其实就类似是一个 while 循环,挂起等待结束之后 线程B 就重新执行循环体的内容,也就是重复上面的操作去竞争锁!
多个线程也是一样的, 线程B 的情况如下图所示:

解锁的伪代码如下:

相比于加锁,其实解锁是很简单的,为什么呢,因为一般执行到解锁的时候,都是单个线程来解锁的,基本不会遇到说多个线程来解锁的情况,因为在此之前我们已经加锁过了!
这里就不过多描述了,和加锁其实是一样的,只不过这里是反过来!
首先加锁的线程,也就是上面的 线程A 来执行这些语句,第一句就是 moveb $1 mutex,它的意思是将 1 写到 mutex 中,还记得我们上面说 mutex 这个状态数据的表示含义吗,1 表示没加锁,可以获取;0 表示加锁中,不能被获取。
所以将 mutex 的值改为了 1,此时就是表示解锁!然后就会唤醒那些挂起等待中的线程,最后 return 0。
所以可以看出解锁并不是原子性的!
Ⅴ. 封装锁对象 && 守卫锁
有时候我们可能想简单的使用锁来进行互斥,那么我们可以将锁进行简单的封装,其实不难,下面我们写一个简单的封装的锁对象:
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
Mutex(pthread_mutex_t* mtx)
:_mtx(mtx)
{}
void lock()
{
if(_mtx != nullptr)
pthread_mutex_lock(_mtx);
}
void unlock()
{
if(_mtx != nullptr)
pthread_mutex_unlock(_mtx);
}
~Mutex()
{}
private:
pthread_mutex_t* _mtx; // 线程库中的锁对象指针
}; 其实有时候我们不仅仅是简单的封装锁对象,在考虑一些情况,比如说当我们加锁的区间,也就是临界区发生了一些情况而导致提前退出了这个函数,但是该线程由于提前退出而导致没有解锁,这时候就会导致死锁的问题(死锁后面我们会讲),这个时候我们就需要一个守卫锁来解决这个问题!
守卫锁其实是利用了 RAII 思想,也就是我们创建的锁,利用一个类或者结构体封装起来,这个类的构造函数中进行加锁,析构函数中进行解锁!
这样子就能保证就算加锁后因为异常情况提前退出函数,这个类也会调用析构函数进行解锁,防止死锁!
下面是简单的守卫锁实现(利用的是上面封装的锁对象):
class LockGuard
{
public:
LockGuard(pthread_mutex_t* lock)
:_guard(lock)
{
_guard.lock(); // 构造函数内加锁
}
~LockGuard()
{
_guard.unlock(); // 析构函数内解锁
}
private:
Mutex _guard; // 封装的锁对象
}; 现在我们在之前写的抢票代码中使用这个守卫锁来防止死锁情况(这里只给出线程执行函数代码,其它位置不变):
#include "Mutex.hpp"
void* thread_routine(void* args)
{
ThreadData* td = static_cast<ThreadData*>(args);
while(true)
{
// 为了让锁只对下面的抢票进行加锁,而不影响后面的抢票完的处理操作
// 这里用{}也就是代码块将下面加锁的区域括起来即可!
{
LockGuard lock(td->_mtx);
// pthread_mutex_lock(td->_mtx);
if(ticket > 0)
{
usleep(12345); // 1秒=10^3毫秒=10^6微秒
cout << "new thread -> name: " << td->_name << ",the ticket: " << ticket << endl;
ticket--;
// pthread_mutex_unlock(td->_mtx); // 释放锁
}
else
{
// pthread_mutex_unlock(td->_mtx); // 记得不满足也要释放锁,不然直接break会造成死锁
break;
}
}
usleep(1000);
}
}Ⅵ. 重入&&线程安全
一、概念
-
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
-
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
① 常见的可重入的情况
- 不使用全局变量或静态变量。
- 不使用用
malloc或者new开辟出的空间。 - 不调用不可重入函数。
- 不返回静态或全局数据,所有数据都有函数的调用者提供。
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
② 常见的不可重入的情况
- 调用了
malloc/free函数,因为malloc函数是用全局链表来管理堆的。 - 调用了标准
I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构。 - 可重入函数体内使用了静态的数据结构。
③ 常见的线程安全的情况
- 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的。
- 类或者接口对于线程来说都是原子操作。
- 多个线程之间的切换不会导致该接口的执行结果存在二义性。
④ 常见的线程不安全的情况
- 不保护共享变量的函数。
- 函数状态随着被调用,状态发生变化的函数。
- 返回指向静态变量指针的函数。
- 调用线程不安全函数的函数。
二、重入与线程安全的联系
- 函数是可重入的,那就是线程安全的。
- 函数是不可重入的,那就不能由多个线程使用,因为有可能引发线程安全问题。
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
三、重入与线程安全的区别
- 可重入函数是线程安全函数的一种。
- 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。