1、线程安全
(1)概念:
线程安全的概念比较直观。一般说来,一个函数被称为线程安全的,当且仅当被多个并发线程反复调用时,它会一
直产生正确的结果。如果你的程序所在的进程中有多个线程在同时运行,而这些线程可能同时运行一段代码或同时访
问一个对象,如果每次运行完这段代码或访问完这个对象之后,所得到的结果和单线程运行的结果一样,而其他变量
的值也和预期的保持一致,那么就认为是线程安全的。 也就是说当多个线程同时运行同一段代码,不会造成资源的
冲突,不会产生错误的结果就是线程安全的。如果有一段线程安全的代码(原子操作或线程间切换不会导致结果的二
义性),它在多个线程中使用是不需要作同步处理的。
线程不安全的后果:
线程不安全可能导致的后果是显而易见的共享变量的值由于不同线程的访问,可能发生不可预料的变化,进而导致
程序的错误,甚至崩溃,线程不安全的代码在多线程环境中必须作同步处理。
我们定义四类(有相交的)线程不安全函数。
1)不保护共享变量的函数
将这类线程不安全函数变为线程安全的,相对比较容易:利用像P和V操作这样的同步操作来保护共享变量。这个
方法的优点是在调用程序中不需要做任何修改,缺点是同步操作将减慢程序的执行时间。
2)保持跨越多个调用的状态函数
3)返回指向静态变量指针的函数
某些函数(如gethostbyname)将计算结果放在静态结构中,并返回一个指向这个结构的指针。如果我们从并发
线程中调用这些函数,那么将可能发生灾难,因为正在被一个线程使用的结果会被另一个线程悄悄地覆盖了。
有两种方法来处理这类线程不安全函数。一种是选择重写函数,使得调用者传递存放结果的结构地址。这就消除了所
有共享数据,但是它要求程序员还要改写调用者的代码。如果线程不安全函数是难以修改或不可修改的(例如,它是
从一个库中链接过来的),那么另外一种选择就是使用lock-and-copy(加锁-拷贝)技术。这个概念将线程不安全函
数与互斥锁联系起来。在每个调用位置,对互斥锁加锁,调用函数不安全函数,动态地为结果非配存储器,拷贝函数
返回的结果到这个存储器位置,然后对互斥锁解锁。一个吸引人的变化是定义了一个线程安全的封装(wrapper)函
数,它执行lock-and-copy,然后调用这个封转函数来取代所有线程不安全的函数。
4)调用线程不安全函数的函数
如果函数f调用线程不安全函数g,那么f就是线程不安全的吗?不一定。如果g是类2类函数,即依赖于跨越多次调
用的状态,那么f也是不安全的,而且除了重写g以外,没有什么办法。然而如果g是第1类或者第3类函数,那么只要
用互斥锁保护调用位置和任何得到的共享数据,f可能仍然是线程安全的。
一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在
二义性,也就是说我们不用考虑同步的问题。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操
作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就
可能影响线程安全。一般来说,一个函数被称为线程安全的,当且仅当被多个并发线程反复调用时,它会一直产生正
确的结果。
根据线程的同步与互斥,也就是当两个线程同时访问到同一个临界资源的时候,如果对临界资源的操作不是原子
的就会产生冲突,使得结果并不如最终预期的那样。
(2)如何避免线程安全?
要避免线程安全,主要需要考虑的是线程之间的共享变量。属于同一进程的不同线程会共享进程内存空间中的全
局区和堆,而私有的线程空间则主要包括栈和寄存器。因此,对于同一进程的不同线程来说,每个线程的局部变量都
是私有的,而全局变量、局部静态变量、分配于堆的变量都是共享的。在对这些共享变量进行访问时,如果要保证线
程安全,则必须通过加锁的方式。
2、可重入函数
(1)概念:
在多线程或有异常控制流的情况下,当某个函数运行到中途时,控制流(也就是当前指令序列)就有可能被打断而去执
行另一个函数.而"另一个函数"很有可能是它本身.,如果在这种情况下不会出现问题,比如说数据或状态不会被破坏,
行为确定。那么这个函数就被称做"可重入"的.函数是可重入(reentrant)的,是指对于相同的(并且合法的)函数
参数(包括无参函数的情况),多次重复调用此函数产生的行为是可预期的,即函数的行为一致,或者结果相同。不
能保证这一点的函数称为不可重入(non-reentrant)函数。简单的描述为:
可重入函数
1)不在函数内部使用静态或全局数据
(2)可重入的判断条件:
要确保函数可重入,需满足一下几个条件:
1)不在函数内部使用静态或全局数据
2)不返回静态或全局数据,所有数据都由函数的调用者提供。
3)使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
4)不调用不可重入函数。
(3)可重入函数的分类
1)显式可重入函数
如果所有函数的参数都是传值传递的(没有指针),并且所有的数据引用都是本地的自动栈变量(也就是说没有
引用静态或全局变量),那么函数就是显示可重入的,也就是说不管如何调用,我们都可断言它是可重入的。
2)隐式可重入函数
可重入函数中的一些参数是引用传递(使用了指针),也就是说,在调用线程小心地传递指向非共享数据的指针
时,它才是可重入的。 可重入函数可以有多余一个任务并发使用,而不必担心数据错误,相反,不可重入函数不
能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在 代码的关键部分禁用中断)。可重入函
数可以在任意时刻被中断,稍后再继续运行,不会丢失数据,可重入函数要么使用本地变量,要么在使用全局变量时
保护自己 的数据。
(4)一个可重入函数需要满足的是:
1)不使用全局变量或静态变量;
2)不使用用malloc或者new开辟出的空间;
3)不调用不可重入函数;
4)不返回静态或全局数据,所有数据都有函数的调用者提供;
5)使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据;
(5)不可重入特点
如果一个函数符合以下条件之一的,则是不可重入的:
1)调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的。
2)调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
3)可重入体内使用了静态的数据结构。
3.两者的区别和联系
4)线程安全是在多线程情况下引发的,而可重入函数可以在只有一个线程的情况下发生。
5)如果一个函数有全局变量,则这个函数既不是线程安全也不是可重入的。
6)如果一个函数当中的数据全身自身栈空间的,则这个函数即使线程安全也是可重入的。
7)如果将对临界资源的访问加锁,则这个函数是线程安全的;但如果重入函数的话加锁还未释放,则会产生死锁,
因此不能重入。
8)线程安全函数能够使不同的线程访问同一块地址空间,而可重入函数要求不同的执行流对数据的操作不影响结
果,使结果是相同的。