目录
一、概念
- 如果一个函数能被多个线程同时调用且不发生竞态条件,则成为它是线程安全,也叫可重入函数。
- 通俗地说就是多线程程序无论调度顺序怎么样都可以得到正确的结果,运行时程序不出错。
- 可重入也就是在调用一次未执行结果又重新调用。
二、如何保障安全
做法:线程同步、线程安全(可重入)函数解决
1.strtok_r函数举例说明
strtok_r与strtok的不同是添加了第三个参数:**saveptr,局部变量进行标记。
代码实现用线程fun内对数组buff获取每个字符’a‘,'b','c'....。在主线程内对数组arr获取每个字符'1','2','3'...,然后调用线程fun:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
void* fun(void*arg)
{
char buff[]={"a b c d e f g h"};
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;
pthread_create(&id,NULL,fun,NULL);
char arr[]="1 2 3 4 5 6 7 8";
char *ptr=NULL;
char * s=strtok_r(arr," ",&ptr);
while(s!=NULL)
{
printf("main s=%s\n",s);
sleep(1);
s=strtok_r(NULL," ",&ptr);
}
pthread_join(id,NULL);
}
代码结果:符合预期。
用strtok 不是重载函数的版本进行说明:
利用strtok函数对数组按照分隔符,获取单个字符。值得说明的说strtok函数中有个内部指针用来标记走到哪个位置,而该指针的生存期超越函数,是个静态全局变量,在栈上不会被回收。
代码运行结果为:
解析:
为什么除了第一组外,其余的main内打印的是buff数组的字符?
- 因为strtok内只有一个全局变量指针,只有一份指针时,当线程fun启动时,指针指向buff数组的位置,主线程的传递为null,默认指向fun内传递数组名buff的位置,而指针只能记住一个位置,它会指向最后一个赋值的指针,因此,后续main也指向buff,打印buff内的数组。
为什么第一组打印的是正确的?
- 正确打印是因为第一次调用strtok时,fun和main都传了数组名,不用指针自己指向,而后续调用传递的第一个参数是NULL,需要指针自己找位置就出错了
因此解决方案就是引入线程安全版本函数
这些库函数之所以不可重入,主要是因为使用了静态变量。Linux对很多不可重入的库函数提供了对应的可重入版本,在函数名尾部加_r。在多线程程序中调用库函数,一定要使用其可重入版本,否则可能导致预想不到的结果。
2.fork举例说明
用代码实现程序创建了2条线程,即2个执行路径,在其中一条执行路径中fork,fork出对当前路径的进程复制,然后观察for打印的pid与哪个进程的pid一致,也就是查看fork复制后启动的是哪条路经。答案:fork之后,只启用1条执行路径,启用的路径是当前fork所在的执行路径。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
void* fun(void*arg)
{
//fork();
for(int i=0;i<5;i++)
{
printf("fun run pid=%d\n",getpid());
sleep(1);
}
}
int main()
{
pthread_t id;
pthread_create(&id,NULL,fun,NULL);
//fork();
for(int i=0;i<5;i++)
{
printf("main run pid%d\n",getpid());
sleep(1);
}
char *ptr=NULL;
pthread_join(id,NULL);
exit(0);
}
(1)fork在main内执行,主线程main被启用,执行结果如下,打印出主线程main的pid3669,而fun与main线程打印出进程pid3667,其中3668的pid被另一个线程执行耗掉了。
(2)fork在fun内执行,线程函数fun被启用,执行结果如下,fun与main线程打印出进程pid3712,被启动的线程fun打印的线程pid3714,其中3713的pid被另一个线程执行耗掉了。
3.对线程先锁再fork会出现?
如下代码说明在线程中先加锁再执行fork会出现的情况,其中主线程先创建了线程,然后fork,对子进程加锁解锁并输出提示信息,fun函数内执行加锁后休眠5秒,然后解锁并输出执行信息。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/wait.h>
pthread_mutex_t mutex;
void* fun(void*arg)
{
//加锁
pthread_mutex_lock(&mutex);
printf("fun lock\n");
sleep(5);
pthread_mutex_unlock(&mutex);
printf("fun unlock\n");
}
int main()
{
pthread_t id;
pthread_mutex_init(&mutex,NULL);
pthread_create(&id,NULL,fun,NULL);
sleep(1);//保证fun函数线程先加锁
pid_t pid=fork();
if(pid==-1)
{
exit(0);
}
if(pid==0)
{
printf("子进程即将加锁\n");
pthread_mutex_lock(&mutex);
printf("子进程加锁成功\n");
pthread_mutex_unlock(&mutex);
}
else
{
wait(NULL);
printf("main over\n");
}
pthread_join(id,NULL);
exit(0);
}
执行结果如下,进程被阻塞住,fun函数加锁(锁1)后,fork会把锁复制给子进程,复制过去的状态是复制时的状态即复制了一份已经被加锁了的锁(锁2),因此子进程无法对已经加锁的锁(锁2)进行二次加锁,所以即便fun解锁(锁1)之后,子进程内的锁(锁2)还是没有被解除,仍然无法使用。
解决方案:
在fork之前先加锁,此时加锁,并不是访问临界资源,而是检验是否锁是空闲状态,如果是空闲的状态就可以对其加锁:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/wait.h>
pthread_mutex_t mutex;
void* fun(void*arg)
{
//加锁
pthread_mutex_lock(&mutex);
printf("fun lock\n");
sleep(5);
pthread_mutex_unlock(&mutex);
printf("fun unlock\n");
}
void at_lock(void)//加锁
{
pthread_mutex_lock(&mutex);
}
void at_unlock(void)
{
pthread_mutex_unlock(&mutex);
}
int main()
{
pthread_t id;
pthread_atfork(at_lock,at_unlock,at_unlock);//父进程解锁、子进程解锁
pthread_mutex_init(&mutex,NULL);
pthread_create(&id,NULL,fun,NULL);
sleep(1);
pid_t pid=fork();
if(pid==-1)
{
exit(0);
}
if(pid==0)
{
printf("子进程即将加锁\n");
pthread_mutex_lock(&mutex);
printf("子进程加锁成功\n");
pthread_mutex_unlock(&mutex);
}
else
{
wait(NULL);
printf("main over\n");
}
pthread_join(id,NULL);
exit(0);
}