线程安全

线程安全定义:

线程安全是指在多个线程同时访问同一个函数的时候,不需要进行额外的同步,或者在调用方进行任何其他操作,调用这个函数都可以获得正确的结果(达到我们预想的那样),那么这个函数就是线程安全的。

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

线程安全问题都是由全局变量静态变量引起的(线程共享的数据)。

若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。(来自于百度百科)

线程不安全函数

线程安全

Linux中这么多函数都是线程不安全的,除此之外其他函数都是线程安全的。 

简析strtok

从表中可以看出strtok是线程不安全的,那么为什么这个函数是线程不安全的呢?我们一起来看看

#include<string.h>
char *strtok(char *strToken, const char *strDelimit );
                            //根据strDelimit将strToken分割,返回每次分割的元素地址

strtok用法:

strtok将字符串分解为标记,strToken为要分解的字符串,strDelimit分隔符字符串(如果传入字符串,则传入的字符串中每个字符均为分割符)。首次调用时,将要解析的字符串地址作为第一个参数strToken传递进去,之后再次调用要把strToken设成NULL。当strtok函数到达s1的末尾时,就返回NULL。

 strtok实现原理:

当strtok()在参数strToken的字符串中发现参数strDelimit中包含的分割字符时,则会将该字符改为‘\0’ 字符,然后调用成功后会返回指向被分割出片段的指针。大家可能会疑惑,我们只有第一次调用的时候,才传递了要分割的字符串的地址,后续不都是传递了一个NULL吗,哪怎么样才知道我下次分割从那个地址开始?所以strtok函数中存在一个static的指针SAVE_PTR用来保存下一次调用中将作为起始位置的地址。那么如果我们传递的strToken为空值NULL,则函数保存的指针SAVE_PTR在下一次调用中将作为起始位置。我们就可以再进行分割了,分割后的下一次起始位置存在SAVE_PTR中然后返回当前被分割出来的字符串的地址。

概括地说就是当调用strtok时,如果找到分隔符,则将分隔符改为‘/0’并将函数内部的SAVE_PTR指针指向被修改的分隔符的下一位作为下次分隔的起始位置,如果没找到分隔符,则将SAVE_PTR指针指向字符串末尾的‘/0’,下次再调用strtok时,则返回NULL,函数结束。(所有的线程共用这个SAVE_PTR指针,不论你当前线程调用多少次这个函数,这个函数始终只有一个指针指向下一次分隔的位置,所以在某一个线程调用strtok改变其指向,在下一个线程中调用strtok就有可能会出现问题)

strtok缺点

从实现原理我们可以看出,strtok函数是在原字符串本身上进行操作,破坏待分解字符串的完整性,调用前和调用后的strToken已经不同。因此,如果需要在调用该函数后访问原来的s1,就必须传递字符串的一个拷贝。

strtok线程不安全原因

我刚才上文提到,线程不安全的原因大多是因为数据的共享(全局,静态),那么我们可以看到strtok中为了保存下一个需要解析的字符串的地址,使用了一个静态的指针SAVE_PTR,那么这个指针就是根源啦。由于这个SAVE_PTR只有一个,那么,在同一个程序中出现多个解析不同字符串的strtok调用时,各自的字符串的解析就会互相干扰。

strtok测试实例

在两个线程中分割不同的字符串。

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<pthread.h>

void *fun(void *arg)  //函数线程根据空格分割数字串
{
	char buff[]="1 2 3 4 5 6 7 8 9";
	char *p=strtok(buff," ");
	while(p!=NULL)
	{
	    printf("fun:%s\n",p);
	    p=strtok(NULL," ");
	    sleep(1);
	}
}
int main()
{
	pthread_t id;
	int n=pthread_create(&id,NULL,fun,NULL);
	assert(n==0);
	char buff[]="a b c d e f g h i ";   //主线程根据空格分割字母串
	char *p=strtok(buff," ");
	while(p!=NULL)
	{
	    printf("main:%s\n",p);
	    p=strtok(NULL," ");
	    sleep(1);
	}
        pthread_exit(NULL);
}

运行结果

线程安全 可以看出与我们设想的不一样,两个线程同时执行的时候访问同一个SAVE_PTR会互相干扰,造成了这个strtok函数注定是线程不安全滴。

简析strtok_r

POSIX定义了一个线程安全的函数——strtok_r,以此来代替strtok。_r表示可以重入(reentrant)。

什么叫做可重入与不可重入?

可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的,也即不可重入的。

可重入函数与不可重入函数的特点?

可重入函数是线程安全函数的一种,其特点在于它们被多个线程调用时,不会引用任何共享数据。 
可重入函数通常要比不可重入的线程安全函数效率高一些,因为它们不需要同步操作。

可重入函数需要满足

  • 不使用全局变量静态变量; 
  • 不使用用malloc或者new开辟出的空间; 
  • 不调用不可重入函数; 
  • 不返回静态或全局数据,所有数据都有函数的调用者提供; 
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据;

不可重入函数特点(如果一个函数符合以下条件之一的,则是不可重入的)

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的。 
  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构。 
  • 可重入体内使用了静态的数据结构

strtok_r可重入函数实现原理

strtok_r函数它在实现的时候并不是直接调用那个SAVE_PTR指针,而是我们在自己的线程中定义一个栈区指针,将SAVE_PTR值拷贝到我们定义的这个栈区指针(本地),就可以用我自己定义的这个本地指针进行操作,而不会收到其他线程的干扰了。

#include<string.h>
char *strtok_r(char *str, const char *delim, char **saveptr);

str为要分解的字符串,delim为分隔符字符串。char **saveptr参数是一个指向char *的指针变量,用来在strtok_r内部保存切分时的上下文,即保存自己线程里的SAVE_PTR的地址,之后切割的时候切割开始点是从我们自己定义的这个saveptr中获取,不会收到其他线程的影响(因为栈区数据不共享)。

第一次调用strtok_r时,str参数必须指向待提取的字符串,saveptr参数的值可以忽略。连续调用时,str赋值为NULL,saveptr为上次调用后返回的值,不要修改。strtok_r实际上就是将strtok内部隐式保存的SAVE_PTR指针,以参数的形式与函数外部进行交互。由调用者进行传递、保存甚至是修改。需要调用者在连续切分相同源字符串时,除了将str参数赋值为NULL,还要传递上次切分时保存下的saveptr。

strtok_r线程安全函数实例:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<pthread.h>

void *fun(void *arg)
{
	char buff[]="1 2 3 4 5 6 7 8 9";
	char *q=NULL;
	char *p=strtok_r(buff," ",&q);
	while(p!=NULL)
	{
	    printf("fun:%s\n",p);
	    p=strtok_r(NULL," ",&q);   //用q来保存下次切割的起始地址
	    sleep(1);
	}
}
int main()
{
	pthread_t id;
	int n=pthread_create(&id,NULL,fun,NULL);
	assert(n==0);
	char buff[]="a b c d e f g h i ";
	char *q=NULL;
	char *p=strtok_r(buff," ",&q);
	while(p!=NULL)
	{
	    printf("main:%s\n",p);
	    p=strtok_r(NULL," ",&q);
	    sleep(1);
	}
	pthread_exit(NULL);

}

运行结果 

线程安全

可以看出,两个线程并发执行,我们并没有做什么同步控制,结果同我们想的是一样的,没有出现任何问题,足以说明这个可重入的strtok_r函数是线程安全的。