可重入与线程安全

原文地址:http://blog.chinaunix.net/u/18369/showart_282045.html

可重入,线程安全

这两个东西的官方定义是什么?有什么区别?

刚刚找到一篇文章专门说这个:http://www.unet.univie.ac.at/aix/aixprggd/genprogc/writing_reentrant_thread_safe_code.htm

编写可重入和线程安全的代码

在单线程的进程中,有且仅有一个控制流。这种进程执行的代码不必是可重入的,或线程安全的。在多线程的程序中,同一个函数或是同一个资源可能被多个控制流并发地访问。为了保证资源的完整性,为多线程程序写的代码必须是可重入的和线程安全的。

本节提供了一些编写可重入和线程安全的代码的信息。

理解可重入和线程安全

可重入和线程安全都是指函数处理资源的方式。可重入和线程安全是两个相互独立的概念,一个函数可以仅是可重入的,可以仅是线程安全的,可以两者都是,可以两者都不是。

可重入

一个可重入的函数不能为后续的调用保持静态(或全局)数据,也不能返回指向静态(或全局)数据的指针。函数中用到的所有的数据,都应该由该函数的调用者提供(不包括栈上的局部数据)。一个可重入的函数不能调用不可重入的函数。

一个不可重入的函数经常可以(但不总是可以)通过它的外部接口和功能识别出来。例如strtok是不可重入的,因为它保存着将被识别为token的字符串。ctime也不是一个可重入的函数,它会返回一个指向静态数据的指针,每次调用都可能覆盖这些数据。

线程安全

一个线程安全的函数通过“锁”来保护共享的资源不被并发地访问。“线程安全”仅关心函数的实现,而不影响它的外部接口。

在C中,局部变量是在栈上动态分配的,因此,任何一个不使用静态数据和其它共享资源的函数就是最平凡的线程安全的。例如,下面这个函数就是线程安全的:

/* thread-safe function */

int diff(int x,int y)

{

        int delta;

        delta=y-x;

        if(delta<0)

                delta=-delta;

        return delta;

}

对全局变量的使用是线程“不安全”的。应该为每一个线程维护一份拷贝,或者将其封装起来,使得对它的访问变成串行的。

使一个函数变成可重入的

在大部分情况下,不可重入的函数修改成可重入的函数时,需要修改函数的对外接口。不可重入的函数不能被多线程的程序调用。一个不可重入的函数,基本上不可能是线程安全的。

返回值

很多不可重入的函数返回一个指向静态数据的指针。这个问题可以有两种解决办法:

    1、返回从堆中分配的空间的地址。在这种情况下,调用者必须负责释放堆中的空间。这种办法的优点是不必修改函数的外部接口,但是不能向后兼容。现存的单线程的程序使用修改后的函数会导致内存泄露(因为它们没有释放空间)。

    2、由调用者提供空间。尽管函数的外部接口需要改变,仍然推荐这种方法。

例如,一个strtoupper函数(个人感觉这个东东写得很儍)一个字符串转换成大写,实现为:

/* non-reentrant function */

char *srttoupper(char *string)

{

        static char buffer[MAX_STR_SIZE];

        int index;

        for(index=0;string[index];index++)

                buffer[index]=toupper(string[index]);

        buffer[index]=0;

        return buffer;

}

该函数既不是可重入的,也不是线程安全的。使用第一种方法将其改写为可重入的:

/* reentrant function (a poor solution)*/

char *strtoupper(char *string)

{

        char *buffer;

        int index;

        buffer=malloc(MAX_STR_SIZE);

        /*error check should be checked*/

        for(index=0;string[index];index++)

                buffer[index]=toupper(string[index]);

        buffer[index]=0;

        return buffer;

}

使用第二种方法解决:

/* reentrant function (a better solution)*/

char *strtoupper_r(char *in_str,char *out_string)

{

        int index;

        for(index=0;in_str[index];index++)

                out_str[index]=toupper(in_str[index]);

        out_str[index]=0;

        return out_str;

}

为后继的调用保持数据

一个可重入的函数不应该为后续的调用保持数据(即后继的调用和本次调用无关),因为下一次调用可能是由不同的线程调用的。如果一个函数需要在连续的调用之间维护一些数据,例如一个工作缓冲区或是一个指针,这些数据(资源)应该由调用这个函数的函数提供。

例如:返回指定字符串中的下一个小写字符的函数。字符串只有在第一次调用时被提供,就像strtok一样。到达末尾时返回0(我认为这个函数写得有问题,姑且明白大意就好):

/* non-reentrant function*/

char lowercase_c(char *string)

{

        static char *buffer;

        static int index;

        char c=0;

        if(string!=NULL){

                buffer=string;

                index=0;

        }

        for(;c=buffer[index];index++){

                 if(islower(c)){

                        index++;

                         break;

                 }

         }

        return c;

}

该函数是不可重入的。要使它改写成可重入的,其中的静态数据应该由它的调用者维护。

/* reentrant function*/

char reentrant_lowercase_c(char *string,int *p_index)

{

        char c=0;

        for(;c=string[*p_index];(*p_index)++){

                if(islower(c)){

                        (*p_index)++;

                         break;

                }

        }

        return c;

}

函数的对外接口和使用方法均改变了。

使一个函数变成线程安全的

在一个多线程的程序中,所有的被多线程调用的函数多必须是线程安全的(或可重入的)。注意,不可重入的函数一般都是线程“不安全”的,然而,将它们改写成可重入的同时,一般就会将它们变成线程安全的。

“锁”住共享资源

使用静态数据或者其它任何共享资源(如文件、终端等)的函数,必须对这些资源加“锁”以实现对它们的串行访问,这样才能成为线程安全的函数。例如:

/* thread-unsafe function*/

int increament_counter()

{

        static int counter=0;

        counter++;

        return counter;

}

/* pseudo-code thread-safe function*/

int increment_counter()

{

        static int counter=0;

        static lock_type counter_lock=LOCK_INITIALIZER;

        lock(counter_lock);

        counter++;

        unlock(counter_lock);

        return counter;

}

在一个使用线程库的多线程程序中,应该使用信号量来串行化共享资源的访问,或者其它“锁”

后面还有两节:

A Workaround for Thread-Unsafe Functions

Reentrant and Thread-Safe Libraries

与本文主题关系不大...

综上所述:

1、可重入:多个线程调用是不会互相影响。例如第一个lowercase_c函数是不可重入的,在另一个线程中第二次调用它时,必将覆盖掉第一个线程中的设定的字符串。

2、线程安全:解决多个线程共享资源的问题


---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


http://www.ibm.com/developerworks/linux/library/l-reent/index.html

Use reentrant functions for safer signal handling

In the early days of programming, non-reentrancy was not a threat to programmers; functions did not have concurrent access and there were no interrupts. In many older implementations of the C language, functions were expected to work in an environment of single-threaded processes.

Now, however, concurrent programming is common practice, and you need to be aware of the pitfalls. This article describes some potential problems due to non-reentrancy of the function in parallel and concurrent programming. Signal generation and handling in particular add extra complexity. Due to the asynchronous nature of signals, it is difficult to point out the bug caused when a signal-handling function triggers a non-reentrant function.

This article:

  • Defines reentrancy and includes a POSIX listing of a reentrant function
  • Provides examples to show problems caused by non-reentrancy
  • Suggests ways to ensure reentrancy of the underlying function
  • Discusses dealing with reentrancy at the compiler level

What is reentrancy?

A reentrant function is one that can be used by more than one task concurrently without fear of data corruption. Conversely, a non-reentrant function is one that cannot be shared by more than one task unless mutual exclusion to the function is ensured either by using a semaphore or by disabling interrupts during critical sections of code. A reentrant function can be interrupted at any time and resumed at a later time without loss of data. Reentrant functions either use local variables or protect their data when global variables are used.

A reentrant function:

  • Does not hold static data over successive calls
  • Does not return a pointer to static data; all data is provided by the caller of the function
  • Uses local data or ensures protection of global data by making a local copy of it
  • Must not call any non-reentrant functions

Don't confuse reentrance with thread-safety. From the programmer perspective, these two are separate concepts: a function can be reentrant, thread-safe, both, or neither. Non-reentrant functions cannot be used by multiple threads. Moreover, it may be impossible to make a non-reentrant function thread-safe.

IEEE Std 1003.1 lists 118 reentrant UNIX® functions, which aren't duplicated here. See Resources for a link to the list at unix.org.

The rest of the functions are non-reentrant because of any of the following:

  • They call malloc or free
  • They are known to use static data structures
  • They are part of the standard I/O library

Signals and non-reentrant functions

A signal is a software interrupt. It empowers a programmer to handle an asynchronous event. To send a signal to a process, the kernel sets a bit in the signal field of the process table entry, corresponding to the type of signal received. The ANSI C prototype of a signal function is:

void (*signal (int sigNum, void (*sigHandler)(int))) (int);

Or, in another representation:

typedef void sigHandler(int);
SigHandler *signal(int, sigHandler *);

When a signal that is being caught is handled by a process, the normal sequence of instructions being executed by the process is temporarily interrupted by the signal handler. The process then continues executing, but the instructions in the signal handler are now executed. If the signal handler returns, the process continues executing the normal sequence of instructions it was executing when the signal was caught.

Now, in the signal handler you can't tell what the process was executing when the signal was caught. What if the process was in the middle of allocating additional memory on its heap using malloc, and you call malloc from the signal handler? Or, you call some function that was in the middle of the manipulation of the global data structure and you call the same function from the signal handler. In the case of malloc, havoc can result for the process, because malloc usually maintains a linked list of all its allocated area and it may have been in the middle of changing this list.

An interrupt can even be delivered between the beginning and end of a C operator that requires multiple instructions. At the programmer level, the instruction may appear atomic (that is, cannot be divided into smaller operations), but it might actually take more than one processor instruction to complete the operation. For example, take this piece of C code:

temp += 1;

On an x86 processor, that statement might compile to:

mov ax,[temp]
inc ax
mov [temp],ax

This is clearly not an atomic operation.

This example shows what can happen if a signal handler runs in the middle of modifying a variable:


Listing 1. Running a signal handler while modifying a variable
#include <signal.h>
#include <stdio.h>

struct two_int { int a, b; } data;

void signal_handler(int signum){
   printf ("%d, %d\n", data.a, data.b);
   alarm (1);
}

int main (void){
 static struct two_int zeros = { 0, 0 }, ones = { 1, 1 };

 signal (SIGALRM, signal_handler);

 data = zeros;

 alarm (1);

while (1)
  {data = zeros; data = ones;}
}

This program fills data with zeros, ones, zeros, ones, and so on, alternating forever. Meanwhile, once per second, the alarm signal handler prints the current contents. (Calling printf in the handler is safe in this program, because it is certainly not being called outside the handler when the signal happens.) What output do you expect from this program? It should print either 0, 0 or 1, 1. But the actual output is as follows:

0, 0
1, 1

(Skipping some output...)

0, 1
1, 1
1, 0
1, 0
...

On most machines, it takes several instructions to store a new value in data, and the value is stored one word at a time. If the signal is delivered between these instructions, the handler might find that data.a is 0 and data.b is 1, or vice versa. On the other hand, if we compile and run this code on a machine where it is possible to store an object's value in one instruction that cannot be interrupted, then the handler will always print 0, 0 or 1, 1.

Another complication with signals is that, just by running test cases you can't be sure that your code is signal-bug free. This complication is due to the asynchronous nature of signal generation.

Non-reentrant functions and static variables

Suppose that the signal handler uses gethostbyname, which is non-reentrant. This function returns its value in a static object:

static struct hostent host; /* result stored here*/

And it reuses the same object each time. In the following example, if the signal happens to arrive during a call to gethostbyname in main, or even after a call while the program is still using the value, it will clobber the value that the program asked for.


Listing 2. Risky use of gethostbyname
main(){
  struct hostent *hostPtr;
  ...
  signal(SIGALRM, sig_handler);
  ...
  hostPtr = gethostbyname(hostNameOne);
  ...
}

void sig_handler(){
  struct hostent *hostPtr;
  ...
  /* call to gethostbyname may clobber the value stored during the call
  inside the main() */
  hostPtr = gethostbyname(hostNameTwo);
  ...
}

However, if the program does not use gethostbyname or any other function that returns information in the same object, or if it always blocks signals around each use, you're safe.

Many library functions return values in a fixed object, always reusing the same object, and they can all cause the same problem. If a function uses and modifies an object that you supply, it is potentially non-reentrant; two calls can interfere if they use the same object.

A similar case arises when you do I/O using streams. Suppose the signal handler prints a message with fprintf and the program was in the middle of an fprintf call using the same stream when the signal was delivered. Both the signal handler's message and the program's data could be corrupted, because both calls operate on the same data structure: the stream itself.

Things become even more complicated when you're using a third-party library, because you never know which parts of the library are reentrant and which are not. As with the standard library, there can be many library functions that return values in fixed objects, always reusing the same objects, which causes the functions to be non-reentrant.

The good news is, these days many vendors have taken the initiative to provide reentrant versions of the standard C library. You'll need to go through the documentation provided with any given library to know if there is any change in the prototypes and therefore in the usage of the standard library functions.

Practices to ensure reentrancy

Sticking to these five best practices will help you maintain reentrancy in your programs.

Practice 1

Returning a pointer to static data may cause a function to be non-reentrant. For example, a strToUpper function, converting a string to uppercase, could be implemented as follows:


Listing 3. Non-reentrant version of 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;
}

You can implement the reentrant version of this function by changing the prototype of the function. This listing provides storage for the output string:


Listing 4. Reentrant version of 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;
}

Providing output storage by the calling function ensures the reentrancy of the function. Note that this follows a standard convention for the naming of reentrant function by suffixing the function name with "_r".

Practice 2

Remembering the state of the data makes the function non-reentrant. Different threads can successively call the function and modify the data without informing the other threads that are using the data. If a function needs to maintain the state of some data over successive calls, such as a working buffer or a pointer, the caller should provide this data.

In the following example, a function returns the successive lowercase characters of a string. The string is provided only on the first call, as with the strtok subroutine. The function returns \0 when it reaches the end of the string. The function could be implemented as follows:


Listing 5. Non-reentrant version of 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;
}

This function is not reentrant, because it stores the state of the variables. To make it reentrant, the static data, the index variable, needs to be maintained by the caller. The reentrant version of the function could be implemented like this:


Listing 6. Reentrant version of 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;
}

Practice 3

On most systems, malloc and free are not reentrant, because they use a static data structure that records which memory blocks are free. As a result, no library functions that allocate or free memory are reentrant. This includes functions that allocate space to store a result.

The best way to avoid the need to allocate memory in a handler is to allocate, in advance, space for signal handlers to use. The best way to avoid freeing memory in a handler is to flag or record the objects to be freed and have the program check from time to time whether anything is waiting to be freed. But this must be done with care, because placing an object on a chain is not atomic, and if it is interrupted by another signal handler that does the same thing, you could "lose" one of the objects. However, if you know that the program cannot possibly use the stream that the handler uses at a time when signals can arrive, you are safe. There is no problem if the program uses some other stream.

Practice 4

To write bug-free code, practice care in handling process-wide global variables like errno and h_errno. Consider the following code:


Listing 7. Risky use of errno
if (close(fd) < 0) {
  fprintf(stderr, "Error in close, errno: %d", errno);
  exit(1);
}

Suppose a signal is generated during the very small time gap between setting the errno variable by the close system call and its return. The generated signal can change the value of errno, and the program behaves unexpectedly.

Saving and restoring the value of errno in the signal handler, as follows, can resolve the problem:


Listing 8. Saving and restoring the value of 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;
}

Practice 5

If the underlying function is in the middle of a critical section and a signal is generated and handled, this can cause the function to be non-reentrant. By using signal sets and a signal mask, the critical region of code can be protected from a specific set of signals, as follows:

  1. Save the current set of signals.
  2. Mask the signal set with the unwanted signals.
  3. Let the critical section of code complete its job.
  4. Finally, reset the signal set.

Here is an outline of this practice:


Listing 9. Using signal sets and signal masks
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 */

Skipping sigsuspend(&zeromask); can cause a problem. There has to be some gap of clock cycles between the unblocking of signals and the next instruction carried by the process, and any occurrence of a signal in this window of time is lost. The function call sigsuspend resolves this problem by resetting the signal mask and putting the process to sleep in a single atomic operation. If you are sure that signal generation in this window of time won't have any adverse effects, you can skip sigsuspend and go directly to resetting the signal.

Dealing with reentrancy at the compiler level

I would like to propose a model for dealing with reentrant functions at the compiler level. A new keyword, reentrant, can be introduced for the high-level language, and functions can be given a reentrant specifier that will ensure that the functions are reentrant, like so:

reentrant int foo();

This directive instructs the compiler to give special treatment to that particular function. The compiler can store this directive in its symbol table and use it during the intermediate code generation phase. To accomplish this, some design changes are required in the compiler's front end. This reentrant specifier follows these guidelines:

  1. Does not hold static data over successive calls
  2. Protects global data by making a local copy of it
  3. Must not call non-reentrant functions
  4. Does not return a reference to static data, and all data is provided by the caller of the function

Guideline 1 can be ensured by type checking and throwing an error message if there is any static storage declaration in the function. This can be done during the semantic analysis phase of the compilation.

Guideline 2, protection of global data, can be ensured in two ways. The primitive way is by throwing an error message if the function modifies global data. A more sophisticated technique is to generate intermediate code in such a way that the global data doesn't get mangled. An approach similar to Practice 4, above, can be implemented at the compiler level. On entering the function, the compiler can store the to-be-manipulated global data using a compiler-generated temporary name, then restore the data upon exiting the function. Storing data using a compiler-generated temporary name is normal practice for the compiler.

Ensuring guideline 3 requires the compiler to have prior knowledge of all the reentrant functions, including the libraries used by the application. This additional information about the function can be stored in the symbol table.

Finally, guideline 4 is already guaranteed by guideline 2. There is no question of returning a reference to static data if the function doesn't have one.

This proposed model would make the programmer's job easier in following the guidelines for reentrant functions, and by using this model, code would be protected against the unintentional reentrancy bug.


Resources

About the author

Dipak provides Level 3 support for Distributed File System (DFS). His work involves kernel- and user-level debugging of dumps and crashes, as well as fixing the reported bugs on the AIX and Solaris platforms. Contact Dipak at dipakjha@in.ibm.com.



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值