12.5 重入

Posted U201013687

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了12.5 重入相关的知识,希望对你有一定的参考价值。


我们在10.6节中讨论了可重入函数以及信号处理函数,线程与信号处理函数在重入方面是非常相似的。多线程的控制的本质也是同时调用相同的函数。

如果一个函数可以被多个线程同时安全地调用,我们称该函数为线程安全的(thread-safe).除了图12.9列出的函数之外,The Single Unix Specification定义的其他函数都是线程安全的,此外,ctermid以及tmpnam函数在传入参数是null指针的时候不能保证是线程安全的,类似地,wcrtomb以及wcsrtombs的mbstate_t参数传入为null指针的时候可能是不安全的。
支持线程安全的实现会在头文件中定义标志_POSIX_THREAD_SAFE_FUNCTIONS,应用程序也可以使用函数sysconf传参_SC_THREAD_SAFE_FUNCTIONS在运行时进行检查。在the Single Unix Specification第四版之前,所有兼容XSI选项的实现都要求支持线程安全函数,在SUSv4中,只要兼容POSIX标准的实现都要求支持线程安全函数。
对于POSIX.1中不是线程安全的函数而言,实现也提供了对应的线程安全的版本,图12.10列出了这些函数的线程安全版本,这些函数的名称与它们的非线程安区版本基本相同,但是在名字最后增加了一个_r,表示这些函数是可以重入的,许多函数并不是线程安全的,因为他们的返回数据存储在静态内存缓冲区中,通过修改这些函数的接口,将它们的接口修改为调用者提供的缓冲区就可以实现线程安全了。

如果一个函数对于多线程而言是可重入的,那么我们称该函数是线程安全的,但是,这并不能说明该函数对于信号处理而言是可重入的。如果一个函数对于异步信号处理而言是可重入的,那么称该函数未异步信号安全的(async-signal safe).在10.6节中的图10.4已经总结除了所有异步信号处理安全的函数。
除了列在12.10中的函数之外,POSIX.1还提供了一种线程安全的方式来管理FILE对象,你可以使用flockfile以及ftrylockfile来获取与给定FILE对象关联的锁,该锁是一个递归锁,因此即是当前线程已经锁定了该锁,即使你再次获取该锁也不会出现死锁的情况,谁然锁的具体实现没有指定,但是所有操作FILE对象的标准IO函数都要求表现得像它们在内部调用了函数flockfile以及funlockfile一样。

  1. #include <stdio.h>
  2. int ftrylockfile(FILE *fp);
  3. return: 0 if OK, nonzero if lock can;t be acquired.
  4. void flockfile(FILE *fp);
  5. void funlockfile(FILE *fp);

虽然标准IO函数已经在其内部实现了线程安全,但是提供锁的操作给应用程序还是非常有用的,这允许应用程序将多个标准IO操作组合成一个原子操作。当然,当处理多个FILE对象的时候,需要注意潜在的死锁风险,需要仔细对锁定进行排序。
如果标准IO程序在内部获取和释放锁,当我们执行一次一个字符的IO操作的时候,性能将会急剧恶化,在这种情况下,我们不能再为每一个字符的读写获取和释放锁,为了减少资源的浪费,提供了一些基于字符的无锁定操作的标准IO版本。

  1. #include <stdio.h>
  2. int getchar_unlocked(void);
  3. int getc_unlocked(FILE *fp);
  4. Both return:the next character if OK, EOF on end of file or error.
  5. int putchar_unlocked(int c);
  6. int putc_unlocked(int c, FILE *fp);
  7. Both return: c if OK, EOF on error.

这四个函数不知简单地进行调用,除非使用函数flockfile(或者是ftrylockfile)以及funlockfile进行包围,否则,可能出现不可预知的后果。
一旦锁定了FILE对象,你就可以多次调用上述函数了,锁定与解锁的开销会被多次的数据读写进行分摊。

Example

图12.11 显示了一个getemv函数的可能的实现(7.9节中由介绍),该版本是不可重入的,如果两个线程同时对其进行访问,他们将会看到不一致的结果,因为所有调用函数getenv的线程的返回结果都会存储在单个静态缓冲区中。

  1. #include <limits.h>
  2. #include <string.h>
  3. #define MAXSTRINGSZ 4096
  4. static char envbuf[MAXSTRINGSZ];
  5. extern char **environ;
  6. char *getenv(const char *name)
  7. {
  8. int i,len;
  9. len = strlen(name);
  10. for(i = 0; environ[i] != NULL; i++)
  11. {
  12. if((strncmp(name, environ[i], len) == 0) && (environ[i][len] == \'=\'))
  13. {
  14. strncpy(envbuf, &environ[i][len+1], MAXSTRINGSZ - 1);
  15. return (envbuf);
  16. }
  17. }
  18. return (NULL);
  19. }

图12.11 getenv函数的一个不可重入版本。
我们在图12.12中展示了一个getenv函数的可重入版本,该版本被称为getenv_r.该函数使用了函数pthread_once来保证thread_init函数只能被一个进程调用一次,而不管同时由多少个线程调用了函数getenv_r.关于函数pthread_once,在12.6节中将会进行更加深入的介绍。

  1. #include <string.h>
  2. #include <errno.h>
  3. #include <pthread.h>
  4. #include <stdlib.h>
  5. extern char **environ;
  6. pthread_mutex_t env_mutex;
  7. static pthread_once_t init_done = PTHREAD_ONCE_INIT;
  8. static void thread_init(void)
  9. {
  10. pthread_mutexattr_t attr;
  11. pthread_mutexattr_init(&attr);
  12. pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
  13. pthread_mutex_init(&env_mutex, &attr);
  14. pthread_mutexattr_destroy(&attr);
  15. }
  16. int getenv_r(const char *name, char *buf, int buflen)
  17. {
  18. int i,len,olen;
  19. pthread_once(&init_done, thread_init);
  20. len = strlen(name);
  21. pthread_mutex_lock(&env_mutex);
  22. for(i = 0; environ[i] != NULL; i++)
  23. {
  24. if((strncmp(name, environ[i], len) == 0) && (environ[i][len] == \'=\'))
  25. {
  26. olen = strlen(&environ[i][len+1]);
  27. if(olen >= buflen)
  28. {
  29. pthread_mutex_unlock(&env_mutex);
  30. return (ENOSPC);
  31. }
  32. strcpy(buf, &environ[i][len+1]);
  33. pthread_mutex_unlock(&env_mutex);
  34. return (0);
  35. }
  36. }
  37. pthread_mutex_unlock(&env_mutex);
  38. return (ENOENT);
  39. }

图12.12 getenv函数的一个可重入版本
为了使得函数getenv_r是可重入的,我们对程序接口进行了修改,以致于调用者必须提供其自身的缓冲区,如此一来,每一个线程都使用了一个不一样的缓冲区,从而避免了线程之间的相互干涉,注意,这并不足以使得该函数成为线程安全的,为了使得使得getenv_r成为线程安全的,我们还需要防止在我们对环境变量进行搜索的时候发生改变,我们可以使用同一个互斥锁来保证get_env_r与putenv函数能够串行访问。
我们也可以使用读写锁来允许多个线程并行访问get_env_r函数,但是并行性的增加并不会对性能由明显改善,原因有二:首先,环境变量列表一般来说并不长,所以在搜索列表的时候,我们并不需要锁定互斥锁很长时间;其次,对函数getenv以及putenv函数的调用次数通常都是很少的,因此,即使我们改善了这两个函数的性能,但是对于应用程序的整体性能并不会由多大影响。
即使我们可以使得函数getenv_r函数成为线程安全的,但是并不意味这该函数对于信号处理函数而言是安全的。如果我们使用了非递归互斥锁,在运行过程中就存在死锁的风险,当信号处理函数中断了一个正在执行getenv_r函数的线程,在线程中可能已经获取到了互斥锁,所以信号处理函数中如果再次尝试获取互斥锁将会造成线程死锁,因此,我们必须使用一个递归锁以防止信号处理函数中发生死锁。问题是,由于pthread函数并不保证是异步信号安全的,因此我们也不能使用它们来编写另一个异步信号安全的函数。





以上是关于12.5 重入的主要内容,如果未能解决你的问题,请参考以下文章

可重入和线程安全

synchronized重入后抛出异常,锁释放了吗

什么是重入程序?

可重入函数的问题解答

可重入函数

ReentrantLock可重入锁在我们的代码中。