可重入(Reentrant) vs 线程安全(Thread-Safe)
可重入(Reentrant) vs 线程安全(Thread-Safe)的区别
在并发编程中,可重入和线程安全是两个容易混淆但本质不同的概念。它们的核心区别在于 作用域 和 并发环境下的行为。
1. 可重入(Reentrant)
定义
一个函数或代码块如果在 同一线程 被多次调用(如递归、中断、信号处理)时能正确执行,就称为 可重入。
关键特性
✅ 同一线程安全:可以被同一线程多次进入而不出错。
❌ 不保证多线程安全:如果多个线程同时调用,可能仍然存在竞争条件。
可重入的条件
-
不依赖全局/静态变量(使用局部变量或参数)。
-
不调用非可重入函数(如
malloc
、printf
在某些实现中不可重入)。 -
不修改共享数据(除非通过锁或原子操作)。
示例
// 可重入函数(仅依赖参数和局部变量) int add(int a, int b) {return a + b; // 无共享状态,可重入 }// 不可重入函数(依赖全局变量) int counter = 0; void increment() {counter++; // 修改全局变量,不可重入 }
2. 线程安全(Thread-Safe)
定义
一个函数或代码块在 多个线程并发调用 时仍能正确执行,称为 线程安全。
关键特性
✅ 多线程安全:多个线程同时调用不会导致数据竞争或逻辑错误。
❌ 不一定是可重入的:线程安全可能通过全局锁实现,但锁的递归调用可能导致死锁(不可重入)。
线程安全的实现方式
-
无共享状态(如只读数据或线程局部存储)。
-
原子操作(如
std::atomic
)。 -
同步机制(如互斥锁、信号量)。
示例
#include <mutex> std::mutex mtx; int shared_counter = 0;// 线程安全函数(通过互斥锁保护) void safe_increment() {mtx.lock();shared_counter++; // 受锁保护,线程安全mtx.unlock(); }// 线程安全但不可重入(同一线程重复加锁会死锁) void unsafe_reentrant() {mtx.lock();// 如果同一线程再次调用,会死锁!mtx.unlock(); }
3. 关键区别对比
特性 | 可重入(Reentrant) | 线程安全(Thread-Safe) |
---|---|---|
作用域 | 同一线程内多次调用 | 多线程并发调用 |
依赖数据 | 避免全局/静态变量 | 可安全访问共享数据 |
实现方式 | 无共享状态或递归锁 | 锁、原子操作、无状态设计 |
是否隐含对方 | ❌ 不保证线程安全 | ❌ 不保证可重入(可能死锁) |
典型场景 | 递归函数、信号处理 | 多线程共享资源访问 |
4. 常见关系
(1) 可重入 + 线程安全
-
无状态函数(如纯函数)既是可重入的,也是线程安全的。
int add(int a, int b) { return a + b; } // 可重入 & 线程安全
(2) 可重入但非线程安全
-
依赖线程局部存储(TLS)的函数是可重入的,但多线程访问 TLS 可能不安全。
thread_local int local_counter = 0; void increment_local() { local_counter++; } // 可重入,但非线程安全(如果跨线程访问 TLS)
(3) 线程安全但不可重入
-
使用非递归锁的函数是线程安全的,但同一线程重复加锁会死锁。
std::mutex mtx; void safe_but_not_reentrant() {mtx.lock();// 同一线程再次调用会死锁!mtx.unlock(); }
(4) 不可重入且非线程安全
-
直接操作全局变量的函数既不可重入,也不线程安全。
int global = 0; void unsafe_func() { global++; } // 非可重入,非线程安全
5. 如何选择?
-
需要递归/信号处理 → 确保 可重入(避免全局数据,或用递归锁)。
-
需要多线程并发 → 确保 线程安全(加锁、原子操作或无状态设计)。
-
既要可重入又要线程安全 → 使用 无状态设计 或 递归锁+原子操作。
总结
-
可重入:关注 同一线程 的重复调用安全性。
-
线程安全:关注 多线程 并发调用的正确性。
-
两者无必然联系,但可通过合理设计同时满足(如无状态函数)。
关键口诀:
🔹 “可重入管自己,线程安全管别人。”
🔹 “无状态通吃,有共享需锁。”