在APUE(第二版)这本书中看到了第12章,12.6中讲线程私有数据。为使每个线程可以独立地访问数据副本,在线程的执行函数中调用pthread_key_create()来为该线程创建一个键,通过这个键再用pthread_getspecific()和pthread_setspecific()去关联到该线程的私有数据。
作者随后在代码12-5中利用线程私有数据来实现了一个线程安全的getenv()。
下面大致解释一下我的问题:
首先是关于getenv()函数的实现机制:
进程应当维护着一个全局变量 char **environ,用来指向每一条环境变量的字符串,同时在原本的getenv的实现里,还应当维护着一个全局的字符串的缓冲区(暂且命名为envbuf),当线程调用getenv()时,系统找到对应的环境变量的字符串,拷贝到全局缓冲区envbuf中,再通过getenv()函数返回给调用者。
如果多个线程同时调用getenv()的话,可能就会出现对envbuf的异步访问,造成返回错误结果。这是多线程下的问题。
作者在代码12-4中对应的实现了getenv_r()函数,通过用户提供的缓冲区来存放返回的环境变量字符串,使得getenv_r变成了一个可重入函数,即线程安全函数。
getenv()的函数声明: char* getenv(const char *name);
getenv_r()的函数声明: int getenv_r(const char *name, char *buf, int buflen);
让每次调用时用户去提供缓冲区无疑是安全的。但需要用户将所有调用getenv的地方都改成对getenv_r的调用。
但随后作者提出,如果接口无法改变,可以使用线程私有数据。于是有了我第二段中说的源代码12-5:
#include <stdio.h> // printf
#include <string.h> // strlen, strncmp, strcpy
#include <limits.h> // ARG_MAX
#include <pthread.h>// support thread
#include <stdlib.h> // free()
extern char **environ;
pthread_key_t key;
pthread_once_t init_done = PTHREAD_ONCE_INIT;
pthread_mutex_t env_mutex = PTHREAD_MUTEX_INITIALIZER;
char *envbuf = NULL;
static void thread_init(void)
{
pthread_key_create(&key, free);
}
char* sgetenv(const char *name)
{
size_t namelen = 0;
char *envbuf = NULL;
int i = 0;
pthread_once(&init_done, thread_init);
pthread_mutex_lock(&env_mutex);
// ensure envbuf has enough memory
envbuf = (char*)pthread_getspecific(key);
if (NULL == envbuf) {
envbuf = (char*)malloc(ARG_MAX);
if (NULL == envbuf
|| pthread_setspecific(key, envbuf) != 0) {
pthread_mutex_unlock(&env_mutex);
return NULL;
}
}
// find environment entry
namelen = strlen(name);
for (i = 0; environ[i] != NULL; ++i) {
if (strncmp(environ[i], name, namelen) == 0
&& '=' == environ[i][namelen]) {
strcpy(envbuf, &environ[i][namelen+1]);
pthread_mutex_unlock(&env_mutex);
return envbuf;
}
}
pthread_mutex_unlock(&env_mutex);
return NULL;
}
int main(int argc, char *argv[])
{
printf("ARG_MAX = %d\n", ARG_MAX);
char *path = sgetenv("PATH");
printf("path = %s\n", path);
return 0;
}
(这里我为了和系统的getenv()区分开,特将函数命名为sgetenv)
pthread_once(&init_done, thread_init)函数的作用是当有多个线程同时调用该函数时,只有一个线程去执行了thread_init()函数。
问题是,作者用pthread_once(&init_done, thread_init)函数使得系统只创建一个私有数据的键,也就是说,整个进程中无论有多少个线程,都只会调用一次thread_init(),也即只调用了一次pthread_key_create(),每次sgeienv()函数返回给用户的都是那个唯一的“私有数据”,那么调用sgetenv()函数的N个线程岂不是都会共享通过那个唯一的私有数据的键而访问到的内存吗?这时的“私有数据”还算是私有数据了吗?这个函数的机制岂不是和最初的,线程“不安全”的getenv是一样的了?都是只维护一个缓冲区,最终还是会发生异步访问的问题。
我认为,不应该调用pthread_once()函数,否则每个线程都会共享同一个key,但如果不调用pthread_once的话,那就每次都直接调用pthread_key_create也不好,那样就意味着每次调用getenv()都会创建一段线程的私有数据。而且必须要把上次的私有数据手动free掉才能保证不发生内存泄露问题。
也就是说,现在的问题是,pthread_once(&init_done, thread_init)函数可以保证这个进程只调用一次thread_init()函数,有没有办法可以使每个线程只调用一次thread_init()函数呢?
不知道我上面的分析是否正确,也请明白的人能讲讲究竟APUE的作者有没有出错?上面的getenv()究竟是不是线程安全的?如果我的想法正确的话,那究竟怎样可以使得每个线程只调用一次 thread_init()函数呢?