可重入和线程安全wiki

可重入和线程安全wiki

可重入代码(Reentrant Code)又称为 “纯代码” (Pure Code),是一种允许多个进程同时访问的代码。为使各个进程所执行的代码完全相同,绝对不允许可重入代码在执行中有任何改变。因此,可重入代码是一种不允许任何进程对它进行修改的代码。但事实上,大多数代码在执行时都可能有些改变,例如,用于控制程序执行次数的变量以及指针、信号量及数组等。为此,在每个进程中,都必须配以局部数据区,把在执行中可能改变的部分拷贝到该数据区,这样,程序在执行时,只需对该数据区(属于该进程私有)中的内容进行修改,并不去改变共享的代码,这时的可共享代码即成为可重入码。

pure可以从函数式编程的角度理解,不修改状态,不依赖外部状态,没有副作用。

不等价。两个概念有交集,但互不包含,这里举例说得很清楚 https://en.m.wikipedia.org/wiki/Reentrancy_(computing)

前言

头两天有空找来Effective C++看看,翻到序,作者表示这本书大部分rule都是针对单线程C++程序制定的,多线程环境可能要斟酌后再采用,然后就突然想起来一个问题,malloc是否线程安全?

按照C Programming Language里的那种示例代码肯定是不安全的,那实际情况是怎么样的?

这个问题随后很快找到答案了,通过不同编译选项能够把符号链接到不同实现的glibc,从而自己选择是否使用线程安全版本的malloc:

https://stackoverflow.com/questions/855763/is-malloc-thread-safe

有意思的是随之而来的一个相关问题:

https://stackoverflow.com/questions/856823/threadsafe-vs-re-entrant

我才注意到,这哥们儿关于malloc的那个提问里threadsafe和reentrant是混用的:

Recently, I asked a question, with title as “Is malloc thread safe?”, and inside that I asked, “Is malloc re-entrant?”
I was under the impression that all re-entrant are thread-safe.
Is this assumption wrong?

但想想我好像也分不太清这俩啥区别,对可重入(reentrant)的认识主要还是关于内核中断处理时重新使能中断带来的一些问题。因此闪过的第一个念头是: 可重入是线程安全的充分条件?毕竟都可以重复进入执行了,应该也就线程安全了吧…?

wiki https://en.wikipedia.org/wiki/Reentrancy_(computing) 上给出了关于两者关系的说明,他们互非充分条件,同时附上了简单的示例代码予以阐明。但当时我只大体看了下代码,没太仔细想其中可能的corner case以及带来的问题。

最近因为毕设需要,了解一些kernel驱动开发特有的问题,其中一条就是关于concurrency与reentrancy,想起来当时没怎么仔细看,才回去好好分析了下实例里面的corner case,就有了这篇文章,算是对这条wiki的一个解释吧。

例子&场景

在谈到formal definition前先看它们各自的一些例子和场景吧,这样好有些感性的认识。

线程安全应该算人尽皆知了,也无需多言

  • 多处理器(MP)下多线程应该算最典型的例子。在同一个进程环境下,运行在不同处理器上的不同线程可能会同时访问临界资源(内存映射是共享的),从而引出诸如临界区、互斥/竞争访问的概念。
  • 即便在单处理器(UP)下上述问题也是存在的,如果没有对临界资源的synchronization,可能处理器在运行一个线程的临界区代码时(由于外部中断触发的调度等等)被切换到相同进程的另一个线程,也进入了相同资源的临界区,从而危害到线程安全(相比MP,UP一个比较好处理的点是memory consistency,但这里按下不表)。

可重入我们分别从用户态和内核态的两个场景来感受。

  • 用户态的场景是callback函数。以一个单线程应用为例,假如timer注册的callback与主线程逻辑都会调用某一个函数func,那么我们就必须考虑这个func函数是否可重入。因为会有这样的问题:线程执行func函数到一半,timer的callback被触发,而callback也调用了func函数,于是func函数被重复进入,也就是重入了。
  • 内核态的场景是中断处理例程(ISR)。当一个线程因中断来临而进入内核态ISR时,我们可以在此时关闭中断来确保中断处理不会被打扰。但ISR往往要调用驱动程序,不同驱动程序执行时间差异很大,而当中断被关闭时,系统对所有外部信号(比如鼠标、键盘输入),因此长时间处于关闭中断状态会大幅影响系统的响应性,俗话说就是卡;同时更高权限级的中断也可能会被低权限级的中断给屏蔽掉 。因此,实际中即便是ISR,也只是在部分必要的time critical区域尽可能短地关闭中断 https://en.wikipedia.org/wiki/Interrupt_handler
    ,随后使能(比如linux的tasklet和bottom-half),从而允许中断的嵌套。在这种情况下,我们就必须要考虑ISR的可重入性,因为当重新使能中断后,可能立马就会有中断来临,从而在当前ISR执行过程中重新进入ISR。

从上面的这些场景我们可以隐约感觉出来,线程安全与可重入说的是执行环境的不同方面。实际上线程安全是一个多线程概念,而可重入是一个单线程概念,甚至早在多任务系统出现前,就已被提出。

Formal Definition

线程安全

https://en.wikipedia.org/wiki/Thread_safety

Thread safety is a computer programming concept applicable to multi-threaded code. Thread-safe code only manipulates shared data structures in a manner that ensures that all threads behave properly and fulfill their design specifications without unintended interaction. There are various strategies for making thread-safe data structures. A program may execute code in several threads simultaneously in a shared address space where each of those threads has access to virtually all of the memory of every other thread. Thread safety is a property that allows code to run in multithreaded environments by re-establishing some of the correspondences between the actual flow of control and the text of the program, by means of synchronization.

可重入

In computing, a computer program or subroutine is called reentrant if multiple invocations can safely run concurrently on a single processor system, where a reentrant procedure can be interrupted in the middle of its execution and then safely be called again (“re-entered”) before its previous invocations complete execution. The interruption could be caused by an internal action such as a jump or call, or by an external action such as an interrupt or signal, unlike recursion, where new invocations can only be caused by internal call.

示例阐述

只看定义比较抽象,wiki也给出了示例来阐述为什么“线程安全并不意味着可重入,反之亦然”:同一个函数的四个不同版本,分别对应“不可重入、非线程安全”、“线程安全、不可重入”、“可重入、非线程安全”、“可重入、线程安全”。示例使用一个C语言函数swap,对两个int指针指向的值进行交换(下面在引用时变量名有略微的改动)。

不可重入、非线程安全

int global;

void swap(int* x, int* y)
{
    global = *x;
    *x = *y;
    *y = global;    
}

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

从线程安全的角度

  • swap整个函数其实形成了关于global(全局共享资源)的一个临界区,若要确保线程安全则我们要保证在语句“global = *x;”执行开始到“*y = global;”执行完这个过程中global不会被其他线程修改。在这个例子中我们没有做任何synchronization,可能出现线程1在写入global后、读取global前其他线程也调用swap,修改了global值,从而无法保证线程安全。

从可重入角度

  • 同样,如果当前线程已经进入临界区,写入了global变量,在重新读取global前中断来临,isr()被调用,那么global就会在当前线程下被ISR修改,从而无法保证可重入性。

线程安全、不可重入

_Thread_local int thread_local;

void swap(int* x, int* y)
{
    thread_local = *x;
    *x = *y;
    *y = thread_local;    
}

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

这里的_Thread_local用于提示编译器这是一个per-thread local变量,每个线程独有,这样一来就确保了每个线程在执行swap函数时访问的thread_local都是独有的,就天然地保证了线程安全(即便是isr()中调用也只会访问当前线程的thread_local)

我们主要看看可重入性

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

可重入、非线程安全

int global;

void swap(int* x, int* y)
{
    /* Save global variable. */
    int func_local;
    func_local = global;

    global = *x;
    *x = *y;      /*If hardware interrupt occurs here then it will fail to keep the value of tmp. So this is also not a reentrant example*/
    *y = global;     /* Hardware interrupt might invoke isr() here. */

    /* Restore global variable. */
    global = func_local;
}

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

这个版本的代码涉及两个变量,全局变量global和栈上的临时变量func_local。

我们先分析可重入性。为此,我们只考虑单线程的执行环境

  • 当条件限制在单线程执行环境下时,swap显然成为可重入的了,因为此时我们虽然仍touch到了共享变量global,对其进行了修改,但这个修改是“嵌套”的,因为此时是单线程环境:当线程执行流进入临界区写入global后,即便中断来临,我们也可以确保当重新回到现场时,global仍然是原来的值,因为是单线程,回到线程现场前isr()必然是已经执行完了的。类似地,isr()执行时发生嵌套中断也无妨,因为最内层嵌套总是先于外层嵌套执行完。

然后是线程安全

  • 与最开始的那个版本类似,我们没有对临界区做任何保护,仍然可能线程1刚执行完“global =*x;”接着线程2也对global进行写入,而在线程2使用它的func_local恢复global前线程1就执行了"*y = global;",从而破坏线程安全。

可重入、线程安全

void swap(int* x, int* y)
{
    int func_local;

    func_local = *x;
    *x = *y;
    *y = func_local;    /* Hardware interrupt might invoke isr() here. */
}

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

这个版本的swap清晰明了,除了传递进来的x、y,绝不touch任何共享资源,不论是线程间共享资源还是线程内共享资源(不同的执行流有自己的栈,栈对线程执行或中断执行都是各自私有的,不会互相bother),所以这个函数既可重入又保证了线程安全。


看了上面的四个例子后,我们可以直观地感受到线程安全与可重入是截然不同的两个方面。一个很明显、但可能有点违背直觉(尤其是之前误以为线程安全等于可重入的时候)的推论是:使用互斥、同步锁的函数往往是不可重入的(重复aquire造成死锁),但这种锁却是我们平常拿来确保线程安全的常用技术。

除了锁外,常见的线程安全技术还包括thread-local、immutable等等。而我们刚才就已经看到,thread-local并不能规避可重入问题,因为可重入本身就是单线程内部的共享资源问题。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值