线程安全和可重入函数
1.线程安全
目前的计算机科学中,线程是操作系统调度的最小单元,进程是资源分配的最小单元。当对一个复杂对象进行某种操作时,从操作开始到操作结束,被操作的对象往往会经历若干非法的中间状态。调用一个函数(假设该函数是正确的)操作某对象常常会使该对象暂时陷入不可用的状态(通常称为不稳定状态),等到操作完全结束,该对象才会重新回到完全可用的状态。如果其他线程企图访问一个处于不可用状态的对象,该对象将不能正确响应从而产生无法预料的结果,如何避免这种情况发生是线程安全性的核心问题。
通常线程安全就是多线程访问时,采用加锁机制,当一个线程访问该类的某个数据时,用锁对数据进行保护,其他线程不能访问该数据直到该线程读取完,其他线程才可使用,线程安全不会出现数据不一致或者数据污染。反之, 线程不安全可能导致的后果是显而易见的——共享变量的值由于不同线程的访问,可能发生不可预料的变化,进而导致程序的错误。
线程不安全函数可分为四类:
-
对共享变量的操作非原子的函数
将这类线程不安全函数变为线程安全的,相对比较容易:利用加锁和解锁机制,像P和V操作这样的同步操作来保护共享变量。这个方法的优点是在调用程序中不需要做任何修改,缺点是同步操作将减慢程序的执行时间。
-
保持跨越多个调用的状态的函数
这函数的特点是当前调用的结果依赖于前次调用的中间结果,比如当前线程正执行到一半,被切换,那么它当中的包含对全局变量操作的函数可能会在别的别的线程中被执行,使得他下一次被执行的时候,得到一个不正确的值,解决方法就是程序员在编程的时候避免这种情况。
-
返回指向静态变量或全局变量的指针的函数
这种函数的原因与1和2类似,也是由于被别的线程干扰了静态变量或者全局变量的数据,解决方法也是程序员在编程的时候避免这种情况。
-
调用线程不安全函数的函数
如果函数f调用线程不安全函数g,那么f就是线程不安全的吗?不一定。如果g是类2类函数,即依赖于跨越多次调用的状态,那么f也是不安全的,而且除了重写g以外,没有什么办法。然而如果g是第1类或者第3类函数,那么只要用互斥锁保护调用位置和任何得到的共享数据,f可能仍然是线程安全的。比如上面的gethostbyname_ts。
线程安全总结:
考虑线程安全编程就要避免使用静态或者全局变量,以及返回他们的指针,并且对这种变量的操作也要是原子的,也就是要进行上锁和解锁操作,保证每一个线程在访问的时候是不能被干扰的。
线程安全的函数所调用到的函数也应该是线程安全的。
2.可重入函数
看这样一个实例:
上例中:
向一个链表中头插节点,main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,
- 刚做完第一步的 时候(图中编号1),因为硬件中断使进程切换到内核
- 再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数
- sighandler也调用insert函数向同一个链表head中插入节点node2
- 插入操作的两步都做完之后从sighandler返回内核态(图中3)
- 再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步(图中4)
- 结果是:main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函 数,这称为重入。
insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。
要确保函数可重入,需满足以下几个条件:
- 不在函数内部使用静态或者全局数据
- 不返回静态或者全局数据,所有的数据都由函数调用者提供
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
- 如果必须访问全局数据,使用互斥锁来保护
- 不调用不可重入函数
3.可重入函数与线程安全的联系与区别
两者之间的关系:
- 一个函数对于多个线程是可重入的,则这个函数是线程安全的。
- 一个函数是线程安全的,但并不一定是可重入的。
- 如果一个函数中用到了全局或静态变量,那么它不是线程安全的,也不是可重入的; 如果我们对它加以改进,在访问全局或静态变量时使用互斥量或信号量等方式加锁,则可以使它变成线程安全的,但此时它仍 然是不可重入的,因为通常加锁方式是针对不同线程的访问,而对同一线程可能出现问题;如果将函数中的全局或静态变量去掉,改成函数参数等其他形式,则有可能使函数变成既线程安全,又可重入。
- 如果一个函数当中的数据全身自身栈空间的,则这个函数即使线程安全也是可重入的。
- 如果将对临界资源的访问加锁,则这个函数是线程安全的;但如果重入函数的话加锁还未释放,则会产生死锁,因此不能重入。
- 线程安全函数能够使不同的线程访问同一块地址空间,而可重入函数要求不同的执行流对数据的操作不影响结果,使结果是相同的。