线程安全
- 多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。
- 一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性.
导致线程不安全的原因
- 线程安全问题大多是由全局变量及静态变量引起的
- 局部变量逃逸也可能导致线程安全问题。
线程不安全导致的问题
- 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
线程不安全举例
比如一个 ArrayList 类,在添加一个元素的时候,它可能会有两步来完成:1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。
在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;
而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。
那好,我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是"线程不安全"了。
可重入函数
如果一个函数能被多个线程调用且不发生竞态条件,则我们称它线程安全的,或者说它是可重入函数。
Linux库函数只有一小部分是不可重入的。
strtok()、getservbyname()、getservbyport()、getpwnam()、getgrnam()
一个可重入函数需要满足的
- 不使用全局变量或静态变量
- 不使用用malloc或者new开辟出的空间;
- 不返回静态或全局数据,所有数据都由函数的调用者提供;
- 不调用不可重入函数;
strtok()函数
函数原型:char *strtok(char *str,const char *delim);
作用:作用于字符串str,以delim中的字符为分界符,将str分割成一个个子串,如果s为NULL,则函数保存指针SAVE_PTR在下一次调用中将作为起始位置。
返回值:分割符匹配到的第一个子串
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<pthread.h>
#include<unistd.h>
void *thread_fun(void *arg)
{
char buff[128]={"a b c d e f g h w e"};
char *s=strtok(buff," ");
while(s!=NULL)
{
printf("fun s=%s\n",s);
sleep(1);
s=strtok(NULL," ");
}
}
int main()
{
pthread_t id;
int res=pthread_create(&id,NULL,thread_fun,NULL);
char buff[128]={"1 2 3 4 5 6 7 8 9 10"};
char *s=strtok(buff," ");
while(s!=NULL)
{
printf("main s=%s\n",s);
sleep(1);
s=strtok(NULL," ");
}
pthread_join(id,NULL);
exit(0);
}
由运行结果可知,程序并没有按照我们预想的那样正常运行.
只打印了 主线程中的1,而没有打印其他字符串,是因为字符存储在栈区,而当函数结束,字符区域会被释放掉,再进入到主线程,传NULL,没有字符可分隔,但将函数线程的字符分隔,所以应该将字符数组放到全局变量区,但是放在全局变量是不安全的。
这是由strtok()函数是一个不可重入的函数,之所以不可重入是因为函数内部使用了静态变量。
strtok_r()函数
strtok_r函数是strtok函数的可重入版本。
函数原型:char *strtok_r(char *str,const char *delim,char **saveptr)
char **saveptr参数是一个指向char *的指针变量,用来在strtok_r内部保存切换时的上下文,以应对连续调用分解相同源字符串。
第一次调用strtok_r时,str参数必须指向待提取的字符串,saveptr参数的值可以忽略。从第二次开始调用以后,str倍赋值为NULL,saveptr为上次调用后返回的值,不要修改。
strtok_r实际上就是将strtok内部隐式保存的this指针,以参数的形式与函数外部进行交互。由调用者进行传递、保存甚至是修改。需要调用者在连续切分相同源字符串时,除了将str参数赋值为NULL,还要传递上次切分时保存下的saveptr。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<pthread.h>
#include<unistd.h>
void *thread_fun(void *arg)
{
char buff[128]={"a b c d e f g h w e"};
char* ptr=NULL;
char *s=strtok_r(buff," ",&ptr);
while(s!=NULL)
{
printf("fun s=%s\n",s);
sleep(1);
s=strtok_r(NULL," ",&ptr);
}
}
int main()
{
pthread_t id;
int res=pthread_create(&id,NULL,thread_fun,NULL);
char buff[128]={"1 2 3 4 5 6 7 8 9 10"};
char *ptr=NULL;
char *s=strtok_r(buff," ",&ptr);
while(s!=NULL)
{
printf("main s=%s\n",s);
sleep(1);
s=strtok_r(NULL," ",&ptr);
}
pthread_join(id,NULL);
exit(0);
}
分析代码执行过程:
- 判断参数str是否为NULL,如果是NULL就以传进来的saveptr作为起始分解位置;若不是NULL,则以buff开始切分。
- 跳过待分解字符串开始的所有分界符
- 判断当前待分解的位置是否为’\0’,若是则返回NULL,不是则继续
- 保存当前待分解字符串的指针,在待分解位置以后中找分界符,如果找不到,则将saveptr赋值为待分解字符串尾部’\0’所在的位置;若找的到则将分届符所在位置赋值为’\0’,saveptr指向分界符的下一位。