C++面试题之线程安全与可重入

一.线程安全

线程安全是一种适用于多线程代码的计算机编程概念。线程安全代码仅以确保所有线程正常运行并满足其设计规范的方式操作共享数据结构,而无需意外交互。有多种策略可用于创建线程安全的数据结构。程序的多个线程可以再共享地址空间中同时执行代码,其中这些线程中的每一个都可以访问其他线程的几乎所有内存。线程安全是一种属性,它允许代码在多线程环境中运行,方法是通过同步重新建立实际控制流与程序文本之间的一些对应关系。
也就是说,线程安全问题是由于线程之间存在临界资源(内存映射是共享的),从而引出诸如临界区、互斥/竞争访问的概念。通常的解决方案就是加锁,以保证一个线程在访问临界区时,其他线程不得访问。

二.可重入

重入是指在调用一个函数且没有返回的情况下再次调用此函数,可重入函数是指一个函数发生重入时,不会导致结果的错误。一般情况下重入是不会发生的,只有在发生中断的时候才会发生。中断可能由内部动作(如跳转或调用)或外部动作(如中断或信号)引起,这与递归不同,在递归中,新的调用只能由内部调用引起。按照对重入的定义,递归函数也可以认为是一种重入。
以单线程应用为例(只有主线程),假如timer注册的callback与主线程逻辑都会调用某一个函数func,那么我们就必须考虑这个func函数是否可重入。因为会有这样的问题—线程执行func函数到一半,timer的callback被触发,而callback也调用了func函数,于是func函数被重复进入,也就是重入了。
重入会导致结果错误的原因,同线程安全一样也是由于访问共享变量(全局变量,静态变量等)引起的。但是重入的难点在于无法通过加锁解决,因为一旦在加锁后发生重入,就会导致死锁(因为这是在单线程中的)。因此实现可重入的唯一方法就是避免使用任何共享变量。
在调用函数的时候,会在线程的栈中申请一个函数的栈帧,用于执行这个函数,栈帧中存放的是局部变量,因此如果函数中没有共享变量,函数的执行上下文是完全隔离的,这就等价于线程之间没有共享资源,是不会引发任何问题的。
显然如果函数中调用了不可重入的函数,如(malloc/fprintf),那么此函数一定是不可重入的。虽然malloc和fprintf是不可重入的,但它们是线程安全的。
线程安全与可重入说的是执行环境的不同方面。实际上线程安全是一个多线程概念,而可重入是一个单线程概念,甚至早在多任务系统出现前,就已被提出。因此,很多人将多线程执行同一函数解释为重入,显然这种理解是不合适的。

三.例子

wiki上给出了三个例子,同一个函数swap的三个不同版本,分别对应“不可重入、非线程安全”、“线程安全、不可重入”、“可重入、线程安全”。https://en.wikipedia.org/wiki/Reentrancy_(computing)
1.不可重入、非线程安全

int tmp;

void swap(int* x, int* y)
{
    tmp = *x;
    *x = *y;
    /* Hardware interrupt might invoke isr() here. */
    *y = tmp;    
}

void isr()
{
    int x = 1, y = 2;
    swap(&x, &y);
}

从线程安全的角度
swap整个函数其实形成了关于tmp(全局共享资源)的一个临界区,若要确保线程安全则我们要保证在语句“tmp = *x;”执行开始到“*y = tmp;”执行完这个过程中tmp不会被其他线程修改。在这个例子中我们没有做任何同步,可能出现线程1在写入tmp后、读取tmp前其他线程也调用swap,修改了tmp值,从而无法保证线程安全。
从可重入角度
同样,如果当前线程已经进入临界区,写入了tmp变量,在重新读取tmp前中断来临,isr()被调用,那么tmp就会在当前线程下被ISR修改,从而无法保证可重入性。
注:中断处理例程(ISR)
2.线程安全、不可重入

_Thread_local int tmp;

void swap(int* x, int* y)
{
    tmp = *x;
    *x = *y;
    /* Hardware interrupt might invoke isr() here. */
    *y = tmp;    
}

void isr()
{
    int x = 1, y = 2;
    swap(&x, &y);
}

对任意一个独立的线程,swap函数仍然是不可重入的,因为对于线程执行流与中断执行流来说,tmp仍然是线程内共享的(_Thread_local只是确保了线程间不共享),仍然会发生线程执行流刚进入临界区就被中断打断,tmp遭到中断执行流修改的情况。
3.可重入、线程安全

void swap(int* x, int* y)
{
    int tmp;
    tmp = *x;
    *x = *y;
    *y = tmp;    /* Hardware interrupt might invoke isr() here. */
}

void isr()
{
    int x = 1, y = 2;
    swap(&x, &y);
}

这个版本的swap传递进来的x、y,不会占用任何共享资源,所以这个函数既可重入又保证了线程安全。这里的tmp是栈上而不是全局分配的,栈对线程是本地的,而仅作用于本地数据的函数将始终产生预期的结果。swap没有访问共享数据,因此没有数据竞争。
下面补充一种可重入非线程安全的情况

int tmp;

int add(int a) 
{
    tmp = a;
    return a + 10;
}

该代码仍然有可能在任意地方被中断,但是由于其返回的值与全局变量tmp无关,所以是可重入的。但是这个不是线程安全的的,因为tmp在该调用过程中可能被其他线程修改。

  • 6
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

草上爬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值