【C/C++】对于可重入、线程安全、异步信号安全几个概念的理解

由于前段时间,程序偶尔异常挂起不工作,检查后发现时死锁了,原因就是:在信号处理函数里面调用了fprintf. printf等io函数是需要对输出缓冲区加锁,这类函数对本身是线程安全的,但是对信号处理函数来说是不可重入的(在没有返回之前,不能再次调用),即不是异步信号安全的。
 
对于printf这类函数,可以这样理解:它们使用了全局数据结构(iobuffer),所以不是线程安全的(多个线程同时访问共享资源),也是不可重入的(有共享资源,可能损坏);
 
通过加锁可以变得线程安全,但是仍然不可重入。
 
对于返回值共享的函数可以通过自己传入地址的方式变得线程可以重入(即线程安全)“_r”类函数。
 
如果一个函数返回一个静态地址(如上),同时又使用了全局资源(已经加锁保护),那么即使使用上了的_r方法变得线程可以重入,也不代表信号处理函数可以重入(即异步信号安全)。
 
网上找了下,下面这篇将的还行:
 
对于可重入、线程安全、异步信号安全几个概念的理解
可重入与异步信号安全
一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误。
《多线程编程指南》中定义,可以被信号控制器安全调用的函数被称为"异步信号安全"函数。
因此, 我认为可重入与异步信号安全是一个概念

有人将可重入函数与线程安全函数混为一谈,我认为是不正确的。

这里引用CSAPP中的描述来说明一下:
--------------------------------------------------

CSAPP
13.7.1 线程安全
一个函数被称为线程安全的,当且仅当被多个并发线程反复的调用时,它会一直产生正确的结果。
13.7.2 可重入性
有一类重要的线程安全函数,叫做可重入函数,其特点在于它们具有一种属性:当它们被多个线程调用时,不会引用任何共享的数据。

尽管线程安全和可重入有时会(不正确的)被用做同义词,但是它们之间还是有清晰的技术差别的。可重入函数是线程安全函数的一个真子集。
--------------------------------------------------

重入即表示重复进入,首先它意味着这个函数可以被中断,其次意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括static),这样的函数就是purecode(纯代码)可重入,可以允许有该函数的多个副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。
可重入函数是线程安全函数,但是反过来,线程安全函数未必是可重入函数。
实际上,可重入函数很少,APUE 10.6节中描述了Single UNIX Specification说明的可重入的函数,只有115个;APUE 12.5节中描述了POSIX.1中不能保证线程安全的函数,只有89个。

信号就像硬件中断一样,会打断正在执行的指令序列。信号处理函数无法判断捕获到信号的时候,进程在何处运行。如果信号处理函数中的操作与打断的函数的操作相同,而且这个操作中有静态数据结构等,当信号处理函数返回的时候(当然这里讨论的是信号处理函数可以返回),恢复原先的执行序列,可能会导致信号处理函数中的操作覆盖了之前正常操作中的数据。
不可重入函数的原因在于:
1> 已知它们使用静态数据结构
2> 它们调用malloc和free.
因为malloc通常会为所分配的存储区维护一个链接表,而插入执行信号处理函数的时候,进程可能正在修改此链接表。
3> 它们是标准IO函数.
因为标准IO库的很多实现都使用了全局数据结构

即使对于可重入函数,在信号处理函数中使用也需要注意一个问题就是errno。一个线程中只有一个errno变量,信号处理函数中使用的可重入函数也有可能会修改errno。例如,read函数是可重入的,但是它也有可能会修改errno。因此,正确的做法是在信号处理函数开始,先保存errno;在信号处理函数退出的时候,再恢复errno。

例如,程序正在调用printf输出,但是在调用printf时,出现了信号,对应的信号处理函数也有printf语句,就会导致两个printf的输出混杂在一起。
如果是给printf加锁的话,同样是上面的情况就会导致死锁。对于这种情况,采用的方法一般是在特定的区域屏蔽一定的信号。
屏蔽信号的方法:
1> signal(SIGPIPE, SIG_IGN); //忽略一些信号
2> sigprocmask()
sigprocmask只为单线程定义的
3> pthread_sigmask()
pthread_sigmasks可以在多线程中使用

现在看来信号异步安全和可重入的限制似乎是一样的,所以这里把它们等同看待;-)

线程安全
线程安全:如果一个函数在同一时刻可以被多个线程安全的调用,就称该函数是线程安全的。

不需要共享时,请为每个线程提供一个专用的数据副本。如果共享非常重要,则提供显式同步,以确保程序以确定的方式操作。通过将过程包含在语句中来锁定和解除锁定互斥,可以使不安全过程变成线程安全过程,而且可以进行串行化。

很多函数并不是线程安全的,因为他们返回的数据是存放在静态的内存缓冲区中的。通过修改接口,由调用者自行提供缓冲区就可以使这些函数变为线程安全的。
操作系统实现支持线程安全函数的时候,会对POSIX.1中的一些非线程安全的函数提供一些可替换的线程安全版本。
例如,gethostbyname()是线程不安全的,在Linux中提供了gethostbyname_r()的线程安全实现。
函数名字后面加上"_r",以表明这个版本是可重入的(对于线程可重入,也就是说是线程安全的,但并不是说对于信号处理函数也是可重入的,或者是异步信号安全的)。

多线程程序中常见的疏忽性问题
1> 将指针作为新线程的参数传递给调用方栈。
2> 在没有同步机制保护的情况下访问全局内存的共享可更改状态。
3> 两个线程尝试轮流获取对同一对全局资源的权限时导致死锁。其中一个线程控制第一种资源,另一个线程控制第二种资源。其中一个线程放弃之前,任何一个线程都无法继续
操作。
4> 尝试重新获取已持有的锁(递归死锁)。
5> 在同步保护中创建隐藏的间隔。如果受保护的代码段包含的函数释放了同步机制,而又在返回调用方之前重新获取了该同步机制,则将在保护中出现此间隔。结果具有误导性。对于调用方,表面上看全局数据已受到保护,而实际上未受到保护。
6> 将UNIX 信号与线程混合时,使用sigwait(2) 模型来处理异步信号。
7> 调用setjmp(3C) 和longjmp(3C),然后长时间跳跃,而不释放互斥锁。
8> 从对*_cond_wait() 或*_cond_timedwait() 的调用中返回后无法重新评估条件。


总结
判断一个函数是不是可重入函数,在于判断其能否可以被打断,打断后恢复运行能够得到正确的结果。(打断执行的指令序列并不改变函数的数据)
判断一个函数是不是线程安全的,在于判断其能否在多个线程同时执行其指令序列的时候,保证每个线程都能够得到正确的结果。

如果一个函数对多个线程来说是可重入的,则说这个函数是线程安全的,但这并不能说明对信号处理程序来说该函数也是可重入的。
如果函数对异步信号处理程序的重入是安全的,那么就可以说函数是" 异步-信号安全"的。

参考:
CSAPP
13.7.1 线程安全
13.7.2 可重入性
《Advanced Programming in the UNIX Environment》2nd Editon
10.6 Reentrant Function
12.5 Reentrant
《多线程编程指南》
信号处理程序和异步信号安全
http://blog.chinaunix.net/u/25994/showart_369466.html
阅读终点,创作起航,您可以撰写心得或摘录文章要点写篇博文。去创作
  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
本资源设置1个资源分,您可以下载作为捐献。 如果您有Git,还可以从http://www.goldenhawking.org:3000/goldenhawking/zoom.pipeline直接签出最新版本 (上一个版本“一种可伸缩的全异步C/S架构服务器实现”是有问题的,现在已经完成更改)。 服务由以下几个模块组成. 1、 网络传输模块。负责管理用于监听、传输的套接字,并控制数据流在不同线程中流动。数据收发由一定规模的线程池负责,实现方法完全得益于Qt的线程事件循环。被绑定到某个Qthread上的Qobject对象,其信号-槽事件循环由该线程负责。这样,便可方便的指定某个套接字对象使用的线程。同样,受惠于Qt的良好封装,直接支持Tcp套接字及SSL套接字,且在运行时可动态调整。(注:编译这个模块需要Qt的SSL支持,即在 configure 时加入 -openssl 选项) 2、 任务流水线模块。负责数据的处理。在计算密集型的应用中,数据处理负荷较重,需要和网络传输划分开。基于普通线程池的处理模式,也存在队列阻塞的问题——若干个客户端请求的耗时操作,阻塞了其他客户端的响应,哪怕其他客户端的请求很短时间就能处理完毕,也必须排队等待。采用流水线线程池避免了这个问题。每个客户端把需要做的操作进行粒度化,在一个环形的队列中,线程池对单个客户端,每次仅处理一个粒度单位的任务。单个粒度单位完成后,该客户端的剩余任务便被重新插入到队列尾部。这个机制保证了客户端的整体延迟较小。 3、 服务集群管理模块。该模块使用了网络传输模块、任务流水线模块的功能,实现了跨进程的服务器ßà服务器链路。在高速局域网中,连接是快速、稳定的。因此,该模块被设计成一种星型无中心网络。任意新增服务器节点选择现有服务器集群中的任意一个节点,接入后,通过广播自动与其他服务器节点建立点对点连接。本模块只是提供一个服务器到服务器的通信隧道,不负责具体通信内容的解译。对传输内容的控制,由具体应用决定。 4、 数据库管理模块。该模块基于Qt的插件式数据库封装QtSql。数据库被作为资源管理,支持在多线程的条件下,使用数据库资源。 5、 框架界面。尽管常见的服务运行时表现为一个后台进程,但为了更好的演示服务器的功能,避免繁琐的配置,还是需要一个图形界面来显示状态、设置参数。本范例中,界面负责轮训服务器的各个状态,并设置参数。设置好的参数被存储在一个ini文件中,并在服务开启时加载。 6、应用专有部分模块。上述1-4共四个主要模块均是通用的。他们互相之间没有形成联系,仅仅是作为一种资源存在于程序的运行时(Runtime)之中。应用专有部分模块根据具体任务需求,灵活的使用上述资源,以实现功能。在范例代码中,实现了一种点对点的转发机制。演示者虚拟出一些工业设备,以及一些操作员使用的客户端软件。设备与客户端软件在成功认证并登录后,需要交换数据。改变这个模块的代码,即可实现自己的功能。
C 中异步原理是指程序在执行过程中,可以在某个操作执行完毕之前继续执行其他的操作,而不需要等待该操作完成。这种方式可以提高程序的效率和性能。 在 C 语言中,可以通过多线程、回调函数、事件驱动等方式实现异步操作。具体的实现方式包括以下几个步骤: 1. 创建线程或者使用已有的线程来执行异步操作。可以通过调用 pthread_create() 函数创建线程,并将需要执行的代码作为线程的入口函数。 2. 在异步操作函数中,可以通过设置线程属性来使其在后台运行,不阻塞主线程的执行。 3. 使用互斥锁(mutex)等同步机制来保护共享资源的访问。在异步操作中,可能会涉及到对内存等共享资源的读写操作,为了避免多线程同时访问导致的竞态条件,需要使用互斥锁进行保护。 4. 设置回调函数,在异步操作完成后调用回调函数来处理结果。可以在异步操作函数中使用回调函数指针,当异步操作完成后,调用回调函数来处理结果或者通知主线程。 5. 使用事件驱动的方式来执行异步操作。可以使用 select() 或者 epoll() 等函数来监听特定的事件,当事件发生时,执行相应的处理函数。 总之,C 中异步原理的核心在于通过多线程、回调函数和事件驱动等机制,实现在某个操作执行完毕之前继续执行其他操作,提高程序的效率和性能。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值