线程安全与可重入性

来源:互联网 发布:淘宝装修神器书本 编辑:程序博客网 时间:2024/06/15 04:59

线程安全

当用线程编写程序时,必须小心地编写那些具有称为线程安全性(thread safety)属性的函数。一个函数被称为线程安全的(thread-safe),当且仅当被多个并发(两个或多个时间在同一时间间隔内发生)线程调用时,它会一直产生正确的结果。如果一个函数不是线程安全的,我们就说它是线程不安全的(thread-unsafe)

我们能够定义出四个线程不安全函数类:

第1类:不保护共享变量的函数;

#include<stdio.h>#include<pthread.h>#include<stdlib.h>int g_count = 0;pthread_mutex_t Mutex = PTHREAD_MUTEX_INITIALIZER;//全局锁void* thread_run(void* _val){    int val = 0;    int i = 0;    while(i++ < 5000){//      pthread_mutex_lock(&Mutex);        val = g_count;        printf("thread id : %lu, g_count: %d\n", pthread_self(), g_count);        g_count = val + 1;//      pthread_mutex_unlock(&Mutex);    }    return NULL;}int main(){    pthread_t id0;    pthread_t id1;    pthread_create(&id0, NULL, thread_run, NULL);    pthread_create(&id1, NULL, thread_run, NULL);    pthread_join(id0, NULL);    pthread_join(id1, NULL);    printf("main g_count: %d\n", g_count);    pthread_mutex_destroy(&Mutex);    return 0;}

在上面的代码中,我们可以看到对一个未受保护的全局计数器变量加1。将这类线程不安全函数编程线程安全的是比较容易的,利用P和V操作这样的同步操作来保护或者用互斥锁来保护;这个方法的优点是在调试程序中不需要做任何修改,缺点是同步操作将减缓程序的执行时间;

第2类:保持跨越多个调用的状态函数。

unsigned next_seed = 1;unsigned rand(void){next_seed = next_seed*1103515245 + 12543;    return(unsigned)(next_seed>>16) % 32768;}void srand(unsigned new_seed){    next_seed = new_seed;}                            一个线程不安全的伪随机数生成器

一个伪随机数生成器是这类线程不安全函数的简单例子。rand函数时线程不安全的,因为在当前调用的结果依赖于上次调用的结果。想rand这样的函数线程安全的唯一方法就是重写它。使得它不在使用任何static数据,而是依靠调用者在参数中传递状态信息。在一个大的程序中,可能有成百上千个不同的调用位置,这样做很麻烦,还易出错。

第3类:返回指向静态变量的指针函数。

char *ctime_ts(const time_t *timep, char *privatep){    char *sharedp;    //P(&mutex);    sharedp = ctime(timep);    strcpy(privatep, sharedp);    //V(&mutex);    return privatep;}                        使用加锁-复制技术调用一个第3;类线程不安全函数

上面这个函数,例如ctime和gethost-byname,将计算结果放在一个static变量中,然后返回一个指向这个变量的指针。如果我们从并发线程中调用这些函数,那么将可能发生灾难,因为正在被一个线程使用的结果会被另一个线程悄悄地覆盖了。

有两种方法来处理这类线程不安全函数。一种选择是重写函数,使得调用者传递存放结果的变量的地址。这就消除了所有共享数据,但是它要求程序员能够修改函数的源代码。
如果难以修改或不可能修改的,可以用加锁-复制技术。。基本思想是将线程安全函数与互斥锁联系起来。在每一个调用位置,然后对互斥锁解锁。

第4类:调用线程不安全函数的函数。

如果函数f调用线程不安全函数g,那么f就是线程不安全吗?不一定。如果g是第2类函数,即依赖于跨越多次调用的状态,那么f也是线程不安全的,而且除了重写g以外,没有什么办法。燃文g是第1类或者第3类函数,那么用一个互斥锁保护调用位置和任何得到的共享数据,f仍然可能是线程安全的。

可重入性

有一类重要的线程安全函数,叫做可冲入函数,其特点在于它们具有这样一种属性:当它们被多个线程调用时,不会引用任何共享数据。尽管线程安全和可重入有时会(不正确地)被用作同义词,但是他们之间还是有清晰的技术差别。
这里写图片描述
所有函数的集合被划分为不可相交的线程安全和线程不安全集合。可重入函数集合是线程安全函数的一个真子集。
可重入函数通常要比不可重入的线程安全的函数高效一些,因为他们不需要同步操作。更进一步说,将第2类线程不安全函数转化为线程安全函数唯一方法就是重写它,使之变为可重入的。

int rand_r(unsigned int *nextp){    *nextp = *nextp * 1103515245 + 12345;    return (unsigned int)(*nextp / 65535) % 32768;}

上面的代码就是将第2类中的线程不安全函数写为可重入的线程安全函数;

显式可重入的

如果所有的函数参数都是传值传递的(即没有指针),并且所有的数据引用都是本地的自动栈变量(即没有引用静态或全局变量),那么函数就是显示可重入的,也就是说,无论它是被如何调用的,都可以断言它是可重入的。

隐式可重入的

允许显式可重入函数中一些参数是引用传递的(即允许他们传递指针),那么我们就得到了一个隐式可重入的函数,也就是说,如果调用线程小心地传递指向非共享数据的指针,那么它们它是可重入的。

综上:
我们总使用术语可重入的既包括显式可重入函数也包括隐式可重入函数。可重入性有时既是调用者也是被调用者的属性,并不只是被调用者单独的属性是非常重要的。
Question?
在第3类的函数中c_time_ts函数时线程安全的,但不是可重入的。请解释说明?
ctime_ts函数不是可重入函数,因为每次调用都共享相同的又gethostbyname函数返回的static变量。然而,它是线程安全的,因为对共享变量的访问时被P和V操作保护的,因此是互斥的。

0 0