线程安全:
- 如果一个函数中用到了全局或静态变量,那么它不是线程安全的,也不是可重入的;
- 如果我们对它加以改进,在访问全局或静态变量时使用互斥量或信号量等方式加锁,则可以使它变成线程安全的,但此时它仍然是不可重入的,因为通常加锁方式是针对不同线程的访问,而对同一线程可能出现问题;
- 如果将函数中的全局或静态变量去掉,改成函数参数等其他形式,则有可能使函数变成既线程安全,又可重入。
比如:strtok函数是既不可重入的,也不是线程安全的;加锁的strtok不是可重入的,但线程安全;而strtok_r既是可重入的,也是线程安全的。
确保可重入性的经验
理解这五条最好的经验将帮助您保持程序的可重入性。
经验 1
返回指向静态数据的指针可能会导致函数不可重入。例如,将字符串转换为大写
的 strToUpper
函数可能被实现如下:
清单 3. strToUpper 的不可重入版本
char *strToUpper(char *str) { /*Returning pointer to static data makes it non-reentrant */ static char buffer[STRING_SIZE_LIMIT]; int index; for (index = 0; str[index]; index++) buffer[index] = toupper(str[index]); buffer[index] = '\0'; return buffer; }
通过修改函数的原型,您可以实现这个函数的可重入版本。下面的清单为输出准备了存
储空间:
清单 4. strToUpper 的可重入版本
char *strToUpper_r(char *in_str, char *out_str) { int index; for (index = 0; in_str[index] != '\0'; index++) out_str[index] = toupper(in_str[index]); out_str[index] = '\0'; return out_str; }
由进行调用的函数准备输出存储空间确保了函数的可重入性。注意,这里遵循了标准惯
例,通过向函数名添加“_r”后缀来 命名可重入函数。
经验 2
记忆数据的状态会使函数不可重入。不同的线程可能会先后调用那个函数,并且修改那
些数据时不会通知其他 正在使用此数据的线程。如果函数需要在一系列调用期间维持某
些数据的状态,比如工作缓存或指针,那么 调用者应该提供此数据。
在下面的例子中,函数返回某个字符串的连续小写字母。字符串只是在第一次调用时给
出,如 strtok
子例程。当搜索到字符串末尾时,函数返回 \0
。函数可能如下实现:
清单 5. getLowercaseChar 的不可重入版本
char getLowercaseChar(char *str) { static char *buffer; static int index; char c = '\0'; /* stores the working string on first call only */ if (string != NULL) { buffer = str; index = 0; } /* searches a lowercase character */ while(c=buff[index]){ if(islower(c)) { index++; break; } index++; } return c; }
这个函数是不可重入的,因为它存储变量的状态。为了让它可重入,静态数据,
即 index
, 需要由调用者来维护。此函数的可重入版本可能类似如下实现:
清单 6. getLowercaseChar 的可重入版本
char getLowercaseChar_r(char *str, int *pIndex) { char c = '\0'; /* no initialization - the caller should have done it */ /* searches a lowercase character */ while(c=buff[*pIndex]){ if(islower(c)) { (*pIndex)++; break; } (*pIndex)++; } return c; }
经验 3
在大部分系统中,malloc
和 free
都不是可重入的, 因为它们使用静态数据结构来记录哪
些内存块是空闲的。实际上,任何分配或释放内存的库函数都是不可重入的。这也包括
分配空间存储结果的函数。
避免在处理器分配内存的最好方法是,为信号处理器预先分配要使用的内存。避免在处
理器中释放内存的最好方法是, 标记或记录将要释放的对象,让程序不间断地检查是否
有等待被释放的内存。不过这必须要小心进行,因为将一个对象 添加到一个链并不是原
子操作,如果它被另一个做同样动作的信号处理器打断,那么就会“丢失”一个对象
。不过, 如果您知道当信号可能到达时,程序不可能使用处理器那个时刻所使用的流
,那么就是安全的。如果程序使用的是某些其他流,那么也不会有任何问题。
经验 4
为了编写没有 bug 的代码,要特别小心处理进程范围内的全局变量,
如 errno
和 h_errno
。 考虑下面的代码:
清单 7. errno 的危险用法
if (close(fd) < 0) { fprintf(stderr, "Error in close, errno: %d", errno); exit(1); }
假定信号在 close
系统调用设置 errno
变量 到其返回之前这一极小的时间片段内生成。
这个生成的信号可能会改变 errno
的值,程序的行为会无法预计。
如下,在信号处理器内保存和恢复 errno
的值,可以解决这一问题:
清单 8. 保存和恢复 errno 的值
void signalHandler(int signo){ int errno_saved; /* Save the error no. */ errno_saved = errno; /* Let the signal handler complete its job */ ... ... /* Restore the errno*/ errno = errno_saved; }
经验 5
如果底层的函数处于关键部分,并且生成并处理信号,那么这可能会导致函数不可重
入。通过使用信号设置和 信号掩码,代码的关键区域可以被保护起来不受一组特定信号
的影响,如下:
- 保存当前信号设置。
- 用不必要的信号屏蔽信号设置。
- 使代码的关键部分完成其工作。
- 最后,重置信号设置。
下面是此方法的概述:
清单 9. 使用信号设置和信号掩码
sigset_t newmask, oldmask, zeromask; ... /* Register the signal handler */ signal(SIGALRM, sig_handler); /* Initialize the signal sets */ sigemtyset(&newmask); sigemtyset(&zeromask); /* Add the signal to the set */ sigaddset(&newmask, SIGALRM); /* Block SIGALRM and save current signal mask in set variable 'oldmask' */ sigprocmask(SIG_BLOCK, &newmask, &oldmask); /* The protected code goes here ... ... */ /* Now allow all signals and pause */ sigsuspend(&zeromask); /* Resume to the original signal mask */ sigprocmask(SIG_SETMASK, &oldmask, NULL); /* Continue with other parts of the code */
忽略 sigsuspend(&zeromask);
可能会引发问题。从消除信号阻塞到进程执行下一个 指令之
间,必然会有时钟周期间隙,任何在此时间窗口发生的信号都会丢掉。函数调
用 sigsuspend
通过重置信号掩码并使进程休眠一个单一的原子操作来解决这一问题。如
果您能确保在此时间窗口中生成的信号不会有任何 负面影响,那么您可以忽
略sigsuspend
并直接重新设置信号。