可重入函数 、线程安全

一、线程安全 
线程安全:如果一个函数在同一时刻可以被多个线程安全的调用,就称该函数是线程安全的。不需要共享时,请为每个线程提供一个专用的数据副本。如果共享非常重要,则提供显式同步,以确保程序以确定的方式操作。通过将过程包含在语句中来锁定和解除锁定互斥,可以使不安全过程变成线程安全过程,而且可以进行串行化。 
很多函数并不是线程安全的,因为他们返回的数据是存放在静态的内存缓冲区中的。通过修改接口,由调用者自行提供缓冲区就可以使这些函数变为线程安全的。 
操作系统实现支持线程安全函数的时候,会对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() 的调用中返回后无法重新评估条件。 
3,有这么四类函数称为线程不安全的: 
1>不保护共享变量的函数; 
2>函数状态随着调用改变的函数; 
3>返回指向静态变量指针的函数; 
4>调用线程不安全函数的函数; 
二、可重入函数 
重入即表示重复进入,首先它意味着这个函数可以被中断,其次意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括static),这样的函数就是purecode(纯代码)可重入,可以允许有该函数的多个副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。 
可重入函数是线程安全函数,但是反过来,线程安全函数未必是可重入函数。 
信号就像硬件中断一样,会打断正在执行的指令序列。信号处理函数无法判断捕获到信号的时候,进程在何处运行。如果信号处理函数中的操作与打断的函数的操作相同,而且这个操作中有静态数据结构等,当信号处理函数返回的时候(当然这里讨论的是信号处理函数可以返回),恢复原先的执行序列,可能会导致信号处理函数中的操作覆盖了之前正常操作中的数据。 
不可重入函数的原因在于: 
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可以在多线程中使用 
一个可重入函数需要满足的是: 
1、不使用全局变量或静态变量; 
2、不使用用malloc或者new开辟出的空间; 
3、不调用不可重入函数; 
4、不返回静态或全局数据,所有数据都有函数的调用者提供; 
5、使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据; 
如果一个函数符合以下条件之一则是不可重入的: 
1>调用了malloc或free,因为malloc也是用全局链表来管理堆的。 
2>调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。 
3>SUS规定有些系统函数必须以线程安全的方式实现。

三、总结 
1、判断一个函数是不是可重入函数,在于判断其能否可以被打断,打断后恢复运行能够得到正确的结果。(打断执行的指令序列并不改变函数的数据) 
判断一个函数是不是线程安全的,在于判断其能否在多个线程同时执行其指令序列的时候,保证每个线程都能够得到正确的结果。 
2、如果一个函数对多个线程来说是可重入的,则说这个函数是线程安全的,但这并不能说明对信号处理程序来说该函数也是可重入的。 
3、如果函数对异步信号处理程序的重入是安全的,那么就可以说函数是”异步-信号安全”的。 
4、可重入概念只和函数访问的变量类型有关,和是否使用锁没有关系!而线程安全和锁的使用关系密切,很多时候线程安全是靠锁来保证的。 
函数可以是可重入的,也可以是线程安全的,或者两者皆是,或者两者皆非。不可重入函数不能由多个线程使用。 
5、线程安全是在多线程情况下引发的,而可重入函数可以在只有一个线程的情况下发生。 
6、线程安全不一定是可重入的,而可重入函数则一定是线程安全的。 
7、如果一个函数有全局变量,则这个函数既不是线程安全也不是可重入的。 
8、如果一个函数当中的数据全身自身栈空间的,则这个函数即使线程安全也是可重入的。 
9、如果将对临界资源的访问加锁,则这个函数是线程安全的;但如果重入函数的话加锁还未释放,则会产生死锁,因此不能重入。 
10、线程安全函数能够使不同的线程访问同一块地址空间,而可重入函数要求不同的执行流对数据的操作不影响结果,使结果是相同的。

猜你喜欢

转载自blog.csdn.net/coolwriter/article/details/80208657