线程安全:
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和 运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。
线程安全问题都是由全局变量及静态变量引起的。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
可重入函数:
可重入函数可以有多余一个任务并发使用,而不必担心数据错误,相反,不可重入函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据,可重入函数要么使用本地变量,要么在使用全局变量时保护自己的数据。
可重入函数的条件:
1.不在函数内部使用静态或全局数据
2.不返回静态或全局数据,所有数据都有函数的调用者提供
3.使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
4.不调用不可重入函数
可重入函数和线程安全:
可重入函数与线程安全并不相同,一般来说,可重入的函数一定是线程安全的,但反过来不一定成立,关系可有下图解释。
我们可以采用下面的变化过程来进一步说明上图:
- 如果一个函数中用到了全局或静态变量,那么它不是线程安全的,也不是可重入的;
- 如果我们对它加以改进,在访问全局或静态变量时使用互斥量或信号量等方式加锁,则可以使它变成线程安全的,但此时它仍然是不可重入的,因为通常加锁方式是针对不同线程的访问,而对同一线程可能出现问题;
- 如果将函数中的全局或静态变量去掉,改成函数参数等其他形式,则有可能使函数变成既线程安全,又可重入。
比如:strtok函数是既不可重入的,也不是线程安全的;加锁的strtok不是可重入的,但线程安全;而strtok_r既是可重入的,也是线程安全的。
确保可重入性的经验
理解这五条最好的经验将帮助您保持程序的可重入性。
经验 1
返回指向静态数据的指针可能会导致函数不可重入。例如,将字符串转换为大写
的 strToUpper
函数可能被实现如下:
清单 3. strToUpper 的不可重入版本
通过修改函数的原型,您可以实现这个函数的可重入版本。下面的清单为输出准备了存
储空间:
清单 4. strToUpper 的可重入版本
由进行调用的函数准备输出存储空间确保了函数的可重入性。注意,这里遵循了标准惯
例,通过向函数名添加“_r”后缀来 命名可重入函数。
经验 2
记忆数据的状态会使函数不可重入。不同的线程可能会先后调用那个函数,并且修改那
些数据时不会通知其他 正在使用此数据的线程。如果函数需要在一系列调用期间维持某
些数据的状态,比如工作缓存或指针,那么 调用者应该提供此数据。
在下面的例子中,函数返回某个字符串的连续小写字母。字符串只是在第一次调用时给
出,如 strtok
子例程。当搜索到字符串末尾时,函数返回 \0
。函数可能如下实现:
清单 5. getLowercaseChar 的不可重入版本
这个函数是不可重入的,因为它存储变量的状态。为了让它可重入,静态数据,
即 index
, 需要由调用者来维护。此函数的可重入版本可能类似如下实现:
清单 6. getLowercaseChar 的可重入版本
经验 3
在大部分系统中,malloc
和 free
都不是可重入的, 因为它们使用静态数据结构来记录哪
些内存块是空闲的。实际上,任何分配或释放内存的库函数都是不可重入的。这也包括
分配空间存储结果的函数。
避免在处理器分配内存的最好方法是,为信号处理器预先分配要使用的内存。避免在处
理器中释放内存的最好方法是, 标记或记录将要释放的对象,让程序不间断地检查是否
有等待被释放的内存。不过这必须要小心进行,因为将一个对象 添加到一个链并不是原
子操作,如果它被另一个做同样动作的信号处理器打断,那么就会“丢失”一个对象
。不过, 如果您知道当信号可能到达时,程序不可能使用处理器那个时刻所使用的流
,那么就是安全的。如果程序使用的是某些其他流,那么也不会有任何问题。
经验 4
为了编写没有 bug 的代码,要特别小心处理进程范围内的全局变量,
如 errno
和 h_errno
。 考虑下面的代码:
清单 7. errno 的危险用法
假定信号在 close
系统调用设置 errno
变量 到其返回之前这一极小的时间片段内生成。
errno
的值,程序的行为会无法预计。
如下,在信号处理器内保存和恢复 errno
的值,可以解决这一问题:
清单 8. 保存和恢复 errno 的值
经验 5
如果底层的函数处于关键部分,并且生成并处理信号,那么这可能会导致函数不可重
入。通过使用信号设置和 信号掩码,代码的关键区域可以被保护起来不受一组特定信号
的影响,如下:
- 保存当前信号设置。
- 用不必要的信号屏蔽信号设置。
- 使代码的关键部分完成其工作。
- 最后,重置信号设置。
下面是此方法的概述:
清单 9. 使用信号设置和信号掩码
忽略 sigsuspend(&zeromask);
可能会引发问题。从消除信号阻塞到进程执行下一个 指令之
间,必然会有时钟周期间隙,任何在此时间窗口发生的信号都会丢掉。函数调
用 sigsuspend
通过重置信号掩码并使进程休眠一个单一的原子操作来解决这一问题。如
果您能确保在此时间窗口中生成的信号不会有任何 负面影响,那么您可以忽
略sigsuspend
并直接重新设置信号。