【一文搞懂可重入与线程安全的关系】
一、什么是可重入函数?
在多线程或中断环境中能够被安全调用的函数。
1、不依赖于全局或静态变量:
如果函数使用了全局或静态变量,多个线程或中断可能同时修改它,导致数据竞争。
可重入函数不会使用这些变量,或者在使用时采取保护措施(例如传递局部变量或锁定访问)。
2、不依赖于非线程安全函数:
如果一个函数调用了非线程安全函数,那么它本身也不是可重入的。
3、使用局部变量:
所有需要的数据都存储在局部变量或通过参数传递,以确保线程间互不干扰。
4、避免静态分配的内存:
避免在函数内部使用像 malloc 或 free 这样的静态内存分配方法。
二、两种实现可重入的方式
1. 只使用局部变量
int sum(int a, int b) {
int result = a + b; // 局部变量,线程间互不干扰
return result;
}
下面从汇编层面解释下为什么局部变量是可重入的:
int sum(int a, int b) 函数中的局部变量 result 之所以能在多线程中互不干扰,是因为局部变量 result 的内存空间是在栈帧中分配的,每个线程都有自己独立的栈空间!因此 result 的值在不同线程间不会共享或冲突。汇编如下:(x86-64 gcc 14.2)
上面RBP和RSP是堆栈基地址,EDI和ESI、EDX是局部寄存器,均不存在多线程竞争,唯一需要注意的是EAX,中间可能被别人修改。
函数栈的工作原理【截图自《汇编语言程序设计(美) 布鲁姆》,图虽包浆,但是经典】
如上所示,寄存器EBP是堆栈基址指针,函数参数从右至左依次入栈后,EBP会上移(往高地址方向),ESP则指向原来的EBP。
2. 线程本地存储(Thread-Local Storage, TLS)
thread_local int thread_local_var = 0;
void increment() {
thread_local_var++;
}
每个线程都有自己的 TLS 区域,变量 thread_local_var 在不同线程中是完全独立的,不共享存储空间。由于不同线程的 fs 段寄存器指向不同的 TLS 基址,读取和写入操作只会影响当前线程的变量。
常见的可重入函数
数学函数:如 sin, cos, sqrt,斐波那契函数 等(只读,无全局变量依赖)
内存管理函数:如 memcpy, strlen 等(不修改全局变量)
三、可重入与线程安全是一样的么?
!不一样 !
1.可重入函数
可重入函数是指在任意时刻,当一个函数被多个线程或同一个线程递归调用时,总能正确运行而不产生冲突。
特点:
1、无共享状态:不使用任何非本地的静态或全局变量。
2、无副作用:函数的行为仅依赖传入的参数,不会修改外部环境。
3、局部变量独立:使用的局部变量都保存在函数调用栈上。
4、不依赖线程同步机制。
2.线程安全
线程安全是指在多线程环境中,当多个线程同时调用一个函数或访问某个数据时,不会出现数据竞争或导致程序行为异常。
特点:
1、线程安全的代码可能依赖某些同步机制(如锁、信号量)。
2、可能涉及全局变量或共享资源,但必须通过线程同步机制保证数据一致性。
3、不要求嵌套调用或递归调用一定安全。
3.线程安全的函数一定是可重入的吗?
不一定,线程安全的函数可能依赖锁,导致同一线程递归调用时死锁,失去可重入性。
例:
std::mutex mtx;
void safe_increment(int& counter) {
std::lock_guard<std::mutex> lock(mtx); // 锁定共享资源
counter++;
}
当一个线程调用 mtx.lock() 后,如果同一个线程再次调用 mtx.lock(),会进入死锁状态。
4.可重入的函数一定是线程安全的吗?
一定是,可重入函数没有共享状态,自然不存在数据竞争问题,因此是线程安全的。
可重入函数适用于嵌套调用、底层算法实现(如数学函数 sin、log 等)、不涉及共享资源的独立逻辑。
线程安全函数适用于:多线程并发场景、涉及全局变量或共享资源的访问。
四、可重入锁(递归锁)std::recursive_mutex
上面说到当一个线程调用 mtx.lock() 后,如果同一个线程再次调用 mtx.lock(),会进入死锁状态。
那么解决办法是什么呢?
那就是std::recursive_mutex,其lock() 次数和 unlock() 次数要保证相同,除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同。可以多个线程同时进入,虽然访问了外部变量,但也算是可重入吧。
不过递归锁的使用会带来性能开销,且可能掩盖设计问题,要是能不用就尽量不用吧,出问题了不好定位。