线程安全和可重入函数

来源:互联网 发布:广东专业seo外包公司 编辑:程序博客网 时间:2024/06/05 06:57

一、什么是线程安全

线程安全就是说多个线程访问同一代码,不会产生不确定的结果。换句话说,线程安全就是多线程访问时,采用加锁机制,当一个线程访问该类的某个数据时,用锁对数据进行保护,其他线程不能访问该数据直到该线程读取完,其他线程才能使用,线程安全不会出现数据不一致或者数据污染。反之,线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。

有四类函数成为线程不安全函数
1.不保护共享变量的函数
变为线程安全的方法:利用像P和V操作这样的同步操作来保护共享变量。这个方法的优点是在调用程序中不需要做任何修改。缺点是同步操作将减慢程序的执行时间。

2.函数状态随着调用改变的函数
例子(伪随机数生成器);
`
unsigned int next = 1;

int rand(void)
{
next = next * 1103515245 + 12345;
return (unsigned int)(next / 65536) % 32758;
}

void srand(unsigned int seed)
{
next = seed;
}
`
rand函数是线程不安全的,因为当前的调用的结果依次依赖于前次调用的中间结果。当我们调用了srand为rand设置了一个种子后,我们反复从一个单线程中调用了rand,我们能够预期一个可重复的随机数字序列。但是,如果有多个线程同时调用rand函数,这样的假设就不成立了。

使得rand函数变为线程安全的唯一方式就是重写它,使得它不再使用任何静态数据,取而代之的依靠调用者在参数中传递状态信息。
缺点:程序员现在要被迫改变调用程序的代码。

3.返回指向静态变量指针的函数
某些函数(如gethostbyname)将计算结果放在静态数据结构中,并返回一个指向这个结构的指针。如果我们从并发线程中调用这些函数,那么将可能发生灾难,因为正在被一个线程使用的结果会被另外一个线程悄悄地覆盖了。

有两种方法来处理这类线程不安全的函数:一种是选择重写函数,使得调用者传递存放结果的结构地址。这就消除了所有共享数据,但是它要求程序员还要改写调用者的代码。

如果线程不安全函数是难以修改或者不可修改的(如:他是从一个库中链接过来的),那么另外一种选择是使用lock_and_copy(加锁—拷贝)技术。这个概念将线程不安全函数与互斥锁联系起来。在每个调用位置,对互斥锁加锁,调用线程不安全函数,动态地为结果分配存储器,拷贝函数返回的结果到这个存储器位置,然后对互斥锁解锁。一个吸引人的变化是定义了一个线程安全的封装(wrapper)函数,它执行lock-and_copy,然后调用这个封装函数来取代所有线程不安全的函数。
例如下面的gethostbyname的线程安全函数:

struct hosttent* gethostbyname_ts(char* host){   struct hostent* shared, *unsharedp;   unsharep = Malloc(sizeof(struct hostent));   P(&mutex);   shared = gethostbyname(hostname);   *unsharedp = *shared;   V(&mutex);   return unsharedp; }

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

一个类或者程序所提供的的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。

线程安全问题都是由全局变量及静态变量引起的。若每个线程堆全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的,若有多个线程同时执行写操作,一般需要考虑线程同步,否则的话就会影响线程安全。一般来说,一个函数被称为线程安全的,当且仅当被多个并发线程反复调用时,它会一直产生正确的结果。

根据线程的同步与互斥,也就是当两个线程同时访问到一个临界资源的时候,如果对临界资源的操作不是原子的就会产生冲突,使得结果不是预期的那样,例如:

#include <stdio.h>#include <pthread.h>int val = 0;void* routine(void* arg){    int i = 0;    while(i<400)    {        int tmp = val;        printf("thread is %u;val is :%d\n",pthread_self(),val);        val = tmp + 1;        i++;    }}int main(){    pthread_t a1,a2;    pthread_create(&a1,NULL,routine,NULL);    pthread_create(&a2,NULL,routine,NULL);    pthread_join(a1,NULL);    pthread_join(a2,NULL);    printf("val is :%d\n",val);    return 0;}

运行结果:

这里写图片描述
数值较大时会出错,但是当i<50时会正确输出:

这里写图片描述
此代码是线程不安全的。
因此:线程安全指的是当多个线程访问同一个区域的时候其最终结果是可预期的,并不会因为产生冲突或者异常中断再次恢复而使结果不可预期。

优化,加锁即可!!代码如下:

#include <stdio.h>#include <pthread.h>pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;int val = 0;void* routine(void* arg){    int i = 0;    while(i<1000)    {        pthread_mutex_lock(&lock);        int tmp = val;        printf("thread is %u;val is :%d\n",pthread_self(),val);        val = tmp + 1;        i++;        pthread_mutex_unlock(&lock);    }}int main(){    pthread_t a1,a2;    //pthread_mutex_init(&lock,NULL);    pthread_create(&a1,NULL,routine,NULL);    pthread_create(&a2,NULL,routine,NULL);    pthread_join(a1,NULL);    pthread_join(a2,NULL);    pthread_mutex_destroy(&lock);    printf("val is :%d\n",val);    return 0;}

二、什么是可重入函数

1.可重入函数介绍
先看一个例子:
这里写图片描述
main函数调⽤insert函数向⼀个链表head中插⼊节点node1,插⼊操作分为两步,刚做完第⼀步的 时候,因为硬件中断使进程切换到内核,再次回⽤户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调⽤insert函数向同⼀个链表head中插⼊节点node2,插⼊操作的 两步都做完之后从sighandler返回内核态,再次回到⽤户态就从中断继续 往下执⾏,先前做第⼀步之后被打断,现在继续做完第⼆步。结果是,main函数和sighandler先后 向链表中插⼊两个节点,⽽最后只有⼀个节点(node1)真正插⼊链表中了,node2丢失导致内存泄漏。

像上例这样,Insert函数被不同的控制流程调⽤,有可能在第⼀次调⽤还没返回时就再次进⼊该函数,这称为重⼊, Insert函数访问⼀个全局链表,有可能因为重⼊⽽造成错乱,像这样的函数称为 不可重⼊函数,反之,如果⼀个函数只访问⾃⼰的局部变量或参数,则称为可重⼊(Reentran)函数。

保证函数的可重入性的方法:在写函数时候尽量使用局部变量(例如寄存器、堆栈中的变量),对于要使用的全局变量要加以保护(如采取关中断、信号量等方法),这样构成的函数就一定是一个可重入的函数。

一个可重入函数需要满足的是:
1、不使用全局变量或静态变量;
2、不使用用malloc或者new开辟出的空间;
3、不调用不可重入函数;
4、不返回静态或全局数据,所有数据都有函数的调用者提供;
5、使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据;
如果一个函数符合以下条件之一则是不可重入的:
1>调用了malloc或free,因为malloc也是用全局链表来管理堆的。
2>调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
3>SUS规定有些系统函数必须以线程安全的方式实现。

2.可重入函数的分类

(1)显式可重入函数

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

(2)隐式可重入函数

可重入函数中的一些参数是引用传递(使用了指针),也就是说,在调用线程小心地传递指向非共享数据的指针时,它才是可重入的。 可重入函数可以有多余一个任务并发使用,而不必担心数据错误,相反,不可重入函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在 代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据,可重入函数要么使用本地变量,要么在使用全局变量时保护自己 的数据。

举例:

A. 可重入函数  void strcpy(char *lpszDest, char *lpszSrc) {  while(*lpszDest++=*lpszSrc++);  *dest=0; }B. 不可重入函数1char cTemp;//全局变量void SwapChar1(char *lpcX, char *lpcY) {cTemp=*lpcX;*lpcX=*lpcY; lpcY=cTemp;//访问了全局变量 } C. 不可重入函数2 void SwapChar2(char *lpcX,char *lpcY)  { static char cTemp;//静态局部变量 cTemp=*lpcX; *lpcX=*lpcY;lpcY=cTemp;//使用了静态局部变量 }

原子性操作
当我们决定完成一个任务,通常情况下,在计算机中,看似很简单的任务也是有多个不同的步骤共同完成。该步骤是由cpu 的 一些指令完成的。比如我们常见的 i ++ ;这是一个非原子性操作,因为它先.从内存取出i的值,然后再增1,最后再写入内存中,经过三个步骤完成,如果在中间一个步骤被其他线程影响了,那么就可能出现错误。

所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。

三、线程安全可重入函数的区别

可重入函数是线程安全函数的一种,其特点在于它们被多个线程调用时,不会引用任何共享数据。可重入函数通常要比不可重入的线程安全函数效率高一些,因为它们不需要同步操作。更进一步说,将第2类线程不安全函数(函数状态随着调用改变的函数)转化为线程安全函数的唯一方法就是重写它,使之可重入。

函数可分为:
这里写图片描述
注意
函数可以是可重入的,也可以是线程安全的,或者两者皆是,或者两者皆非。不可重入函数不能由多个线程使用
1、线程安全是在多线程情况下引发的,而可重入函数可以在只有一个线程的情况下发生。

2、线程安全不一定是可重入的,而可重入函数则一定是线程安全的。

3、如果一个函数有全局变量,则这个函数既不是线程安全也不是可重入的。

4、如果一个函数当中的数据全是自身栈空间的,则这个函数即使线程安全也是可重入的。

5、如果将对临界资源的访问加锁,则这个函数是线程安全的;但如果重入函数的话加锁还未释放,则会产生死锁,因此不能重入。

6、线程安全函数能够使不同的线程访问同一块地址空间,而可重入函数要求不同的执行流对数据的操作不影响结果,使结果是相同的。

原创粉丝点击