linux系统编程之基础必备(六):可重入函数、线程安全、volatile

一、 POSIX 中对可重入和线程安全这两个概念的定义:

Reentrant Function:A function whose effect, when called by two or more threads,is guaranteed to be as if 

the threads each executed the function one after another in an undefined order, even if the actual execution is interleaved.

Thread-Safe Function:A function that may be safely invoked concurrently by multiple threads.

 Async-Signal-Safe Function: A function that may be invoked, without restriction from signal-catching functions. No function

 is async-signal -safe unless explicitly described as such.

以上三者的关系为:可重入函数 必然 是 线程安全函数 和 异步信号安全函数; 线程安全函数不一定是可重入函数。

可重入与线程安全的区别体现在能否在signal处理函数中被调用的问题上,可重入函数在signal处理函数中可以被安全调用,因此同时也是Async-

Signal-Safe Function;而线程安全函数不保证可以在signal处理函数中被安全调用,如果通过设置信号阻塞集合等方法保证一个非可重入函数不被信号

中断,那么它也是Async-Signal-Safe Function。

          举个例子,strtok是既不可重入的,也不是线程安全的;加锁的strtok不是可重入的,但线程安全;而strtok_r 既是可重入的,也是线程安全的。也就是说函数如果使用静态变量,通过加锁后可以转成线程安全函数,但仍然有可能不是可重入的。我们所熟知的malloc 也是线程安全但不是可

重入的。

       再举个例子,假设函数func()在执行过程中需要访问某个共享资源,因此为了实现线程安全,在使用该资源前加锁,在不需要资源解锁。 假设该函

数在某次执行过程中,在已经获得资源锁之后,有异步信号发生,程序的执行流转交给对应的信号处理函数;再假设在该信号处理函数中也需要调用函

数 func(),那么func()在这次执行中仍会在访问共享资源前试图获得资源锁,然而我们知道前一个func()实例已然获得该锁,因此信号处理函数阻塞;

另一方面,信号处理函数结束前被信号中断的线程是无法恢复执行的,当然也没有释放资源的机会,这样就出现了线程和信号处理函数之间的死锁局

面。 因此,func()尽管通过加锁的方式能保证线程安全,但是由于函数体对共享资源的访问,因此是非可重入。对于这种情况,采用的方法一般是在特

定的区域屏蔽一定的信号。

二、可重入函数

我们知道,当捕捉到信号时,不论进程的主控制流程当前执行到哪儿,都会先跳到信号处理函数中执行,从信号处理函数返回后再继续执行主控制流程

。信号处理函数是一个单独的控制流程,因为它和主控制流程是异步的,二者不存在调用和被调用的关系,并且使用不同的堆栈空间。

(By default,  the  signal  handler  is invoked on the normal process stack.  It is possible to arrange that the
 signal handler  uses an alternate stack; see sigaltstack(2) for a discussion of how to do this and 
when it might be useful.)

引入了信号处理函数使得一个进程具有多个控制流程,如果这些控制流程访问相同的全局资源(全局变量、硬件资源等),就有可能出现冲突,如下面

的例子所示。

main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户

态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之

后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步。结果

是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。像上例这样,insert函数被不同的控制流程调用,有

可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重

入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant)函数。

不可重入函数的原因在于: 1> 已知它们使用静态数据结构 2> 它们调用malloc和free. 因为malloc通常会为所分配的存储区维护一个链接表,而插入执行信号处理函数的时候,进程可能正在修改此链接表。 3> 它们是标准IO函数. 因为标准IO库的很多实现都使用了全局数据结构

三、volatile 限定符

当变量属于以下情况之一的,需要volatile 限定(嵌入式开发居多):

变量的内存单元中的数据不需要写操作就可以自己发生变化,每次读上来的值都可能不一样;

即使多次向变量的内存单元中写数据,只写不读,也并不是在做无用功,而是有特殊意义的;

什么样的内存单元会具有这样的特性呢?肯定不是普通的内存,而是映射到内存地址空间的硬件寄存器,例如串口的接收寄存器属于上述第一种情况,

而发送寄存器属于上述第二种情况。

对于多线程的程序,访问冲突的问题是很普遍的,解决的办法是引入锁,获得锁的线程可以完成“读-修改-写”的操作,然后释放锁给其它线程,没有获

得锁的线程只能等待而不能访问共享数据,这样“读-修改-写”三步操作组成一个原子操作,要么都执行,要么都不执行,不会执行到中间被打断,也不

会在其它处理器上并行做这个操作。

C 和 C++ 中的volatile 并不是用来解决多线程竞争问题的(也不能确保不发生 reordering),而是用来修饰一些因为程序不可控因素导致变化的变量,

比如访问底层硬件设备的变量,以提醒编译器不要对该变量的访问擅自进行优化。简单的来说,对访问共享数据的代码块加锁,已经足够保证数据访问

的同步性,再加volatile 完全是多此一举。如果光对共享变量使用volatile 修饰而在可能存在竞争的操作中不加锁或使用原子操作对解决多线程竞争没有

任何作用,因为volatile 并不能保证操作的原子性,在读取、写入变量的过程中仍然可能被其他线程打断导致意外结果发生。

本文对原子操作、锁以及volatile的讨论都比较基础,更深入的探讨请看这篇文章

参考:

《linux c 编程一站式学习》

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏张善友的专栏

SSIS数据流

数据流是在SQL Server 2005中才引入的新概念。数据流是专门处理数据操作的工作流。数据流也称为流水线。可以将数据流认为是装配线,该装配线包含了顺序执行...

1909
来自专栏前端学习心得

JavaScript运行机制

1022
来自专栏Java3y

Hibernate【缓存】知识要点

对象状态 Hibernate中对象的状态: 临时/瞬时状态 持久化状态 游离状态 学习Hibernate的对象状态是为了更清晰地知道Hibernate的设计思想...

2635
来自专栏大内老A

WCF技术剖析之二十三:服务实例(Service Instance)生命周期如何控制[上篇]

服务调用的目的体现在对某项服务功能的消费上,而功能的实现又定义在相应的服务类型中。不论WCF服务端框架处理服务调用请求的流程有多么复杂,最终都落实在服务实例的激...

2278
来自专栏Spark学习技巧

Spark调优系列之序列化方式调优

由于大多数的spark计算是基于内存的的天性,spark应用的瓶颈一般受制于集群的CPU,网络带宽,内存。大部分情况下,如果内存适合当前数据量的计算,那么瓶颈往...

2579
来自专栏Java架构师进阶

Mybatis的二级缓存配置

 缓存将使用LRU(Least Recently Used)最近最少使用策略算法来回收

542
来自专栏MasiMaro 的技术博文

windows 线程

在windows中进程只是一个容器,用于装载系统资源,它并不执行代码,它是系统资源分配的最小单元,而在进程中执行代码的是线程,线程是轻量级的进程,是代码执行的最...

512
来自专栏JAVA烂猪皮

BAT面试常的问题和最佳答案

客户端发出http请求,web服务器将请求转发到servlet容器,servlet容器解析url并根据web.xml找到相对应的servlet,并将reques...

792
来自专栏木木玲

设计模式 ——— 状态模式

922
来自专栏陈文啸的专栏

mysql 执行死锁原因排查

今天碰到一次因死锁导致更新操作的sql事务执行时间过长,特将排查过程记录下来。

9580

扫码关注云+社区