Saturday, November 1, 2014

Unix Prog: Thread Reentrancy

1. thread-safe
If a function can be called by multiple threads at the same time, we say that the function is thread-safe. Many functions that are not thread-safe return data stored in a static memory buffer. Thye are made thread-safe by changing their interfaces to require that the caller provide its own buffer.

The function is reentrant respect to multiple threads, which is thread-safe functions, doesn't mean the the function is safe to be re-entered from asynchronous signal handler.

2. FILE lock

System Definition:
 ubuntu@ip-172-31-23-227:~$ less /usr/include/stdio.h  
 ......  
 /* Acquire ownership of STREAM. */  
 extern void flockfile (FILE *__stream) __THROW;  
   
 /* Try to acquire ownership of STREAM but do not block if it is not  
   possible. */  
 extern int ftrylockfile (FILE *__stream) __THROW __wur;  
   
 /* Relinquish the ownership granted for STREAM. */  
 extern void funlockfile (FILE *__stream) __THROW;  
 ......  
 #if defined __USE_POSIX || defined __USE_MISC  
 /* These are defined in POSIX.1:1996.  
   
   These functions are possible cancellation points and therefore not  
   marked with __THROW. */  
 extern int getc_unlocked (FILE *__stream);  
 extern int getchar_unlocked (void);  
 #endif /* Use POSIX or MISC. */  
 ......  
 #if defined __USE_POSIX || defined __USE_MISC  
 /* These are defined in POSIX.1:1996.  
   
   These functions are possible cancellation points and therefore not  
   marked with __THROW. */  
 extern int putc_unlocked (int __c, FILE *__stream);  
 extern int putchar_unlocked (int __c);  
 #endif /* Use POSIX or MISC. */  
 ......  

We can use flockfile and ftrylockfile to obtain a lock associated with a given FILE object, the lock is recursive. All standard I/O routine that manipulate FILE object behave as if they call flockfile and funlockfile internally. But it is useful to expose these functions to compose multiple calls to standard I/O functions into atomic sequences.

For standard I/O routine doing the character-at-a-time I/O, the performance will be seriously impacted if doing the lock and unlock every time. We prefer to do the character-at-a-time I/O without unlocking for each time.
We can use:
getc_unlocked, getchar_unlocked, putc_unlocked, putchar_unlocked

For thread-safe, we need to surround above functions with flockfile/ftrylockfile, and funlockfile.

3. Reentrancy Example
non-reentrant version:
getenv.c:
 #include<stdio.h>  
 #include<stdlib.h>  
 #include<string.h>  
 #include<unistd.h>  
   
 #define ARG_MAX 4096  
   
 static char envbuf[ARG_MAX];  
 extern char **environ;  
   
 char* getenv(const char *name)  
 {  
  int i, len;  
  len = strlen(name);  
  for(i = 0;environ[i] != NULL; i++) {  
   if((strncmp(name, environ[i], len) == 0) &&  
     (environ[i][len] == '=')) {  
    strcpy(envbuf, &environ[i][len+1]);  
    return envbuf;  
   }  
  }  
   
  return NULL;  
 }  
   
 int main(int argc, char* argv[])  
 {  
  printf("SHELL: %s\n", getenv("SHELL"));  
  exit(0);  
 }  

shell:
The function is not thread safe because it is using global static memory.
 ubuntu@ip-172-31-23-227:~$ ./env.out  
 SHELL: /bin/bash  

==========================================================
reentrant version:
 #include<stdio.h>  
 #include<stdlib.h>  
 #include<string.h>  
 #include<unistd.h>  
 #include<pthread.h>  
 #include<errno.h>  
   
 #define ARG_MAX 4096  
   
 extern char **environ;  
 pthread_mutex_t env_mutex;  
 static pthread_once_t init_done = PTHREAD_ONCE_INIT;  
   
 static void thread_init(void)  
 {  
  pthread_mutexattr_t attr;  
  pthread_mutexattr_init(&attr);  
  pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);  
  pthread_mutex_init(&env_mutex, &attr);  
  pthread_mutexattr_destroy(&attr);  
 }  
   
 int getenv_r(const char *name, char* buf, int buflen)  
 {  
  int i, len, olen;  
   
  // Initialize the mutex and mutex attribute, we use pthread_once to  
  // make sure the function is only called once within the process  
  pthread_once(&init_done, thread_init);  
  len = strlen(name);  
   
  // Iterate the environ and return the entry  
  pthread_mutex_lock(&env_mutex);  
  for(i = 0;environ[i] != NULL; i++) {  
   if((strncmp(name, environ[i], len) == 0) &&  
     (environ[i][len] == '=')) {  
    olen = strlen(&environ[i][len+1]);  
    if(olen >= buflen) {  
     pthread_mutex_unlock(&env_mutex);  
     return(ENOSPC);  
    }  
    strcpy(buf, &environ[i][len+1]);  
    pthread_mutex_unlock(&env_mutex);  
    return 0;  
   }  
  }  
   
  // Unlock and return  
  pthread_mutex_unlock(&env_mutex);  
  return ENOENT;  
 }  
   
 int main(int argc, char* argv[])  
 {  
  char buf[ARG_MAX];  
  getenv_r("SHELL", buf, ARG_MAX);  
  printf("SHELL: %s\n", buf);  
  exit(0);  
 }  

shell:
 ubuntu@ip-172-31-23-227:~$ ./env.out  
 SHELL: /bin/bash  

Compared to the previous version, it use its own buffer instead of global static buffer which could be shared by many threads. Also to protect the change against the environment variable, it use the mutex to protect the access to get env process, and we also need to use the mutex to protect the putenv process.

No comments:

Post a Comment