文章目录
4. 线程安全
4.1 概念
线程安全:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,则称其为线程安全的
线程不安全:由于线程是并发运行的,并且一个进程中的线程之间共享地址空间。如果这个多线程的程序运行过程中存在多线程去修改共享的数据时,会造成执行的结果存在二义性
4.2 线程不安全示例
多线程修改共享的数据
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int gdata = 1; // .data段保存的数据
void *fun(void * arg)
{
int i = 0; // fun的栈区定义的变量
for(; i < 10000; ++i)
{
gdata++;
}
}
int main()
{
pthread_t id;
int res = pthread_create(&id, NULL, fun, NULL);
assert(res == 0);
// 主线程和函数线程并发执行的代码
int i = 0; // main的栈区定义的变量
for(; i < 10000; ++i)
{
gdata++; // 1、读取内存当前的值 2、在寄存器上执行++操作 3、写回内存
}
pthread_join(id, NULL);//等待函数线程执行结束
// 以下的代码只有main线程执行
printf("gdata = %d\n", gdata);
exit(0);
}
执行结果如下:
执行结果分析:因为gdata是全局区的数据,主线程和函数线程共享gdata,而++操作不是一个原子操作,分成读改写三步:读取内存当前值,在寄存器上执行++操作,写回内存。所以在执行的时候,就有可能一个线程读了之后,还未写回内存,另一个线程又执行了读,导致结果本来要加2,但是只加了1,使得执行结果不符合我们的预期
对于该类多线程修改共享数据的问题,可以使用线程同步(互斥锁)来解决
4.3 可重入函数
可重入函数:可以被重复进入的函数,即可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误
不可重入函数:由于使用了一些系统资源,比如全局变量区,中断向量表等,因此不能被中断的函数,如果多线程环境下使用可能会出现问题
strtok与strtok_r
strtok()是不保证线程安全的,在多线程程序中,就可能会出现问题,strtok()实现的代码使用了线程间共享的数据,它是不可重入函数
与其对应的保证线程安全的方法: strtok_r(),strtok_r()就是可重入函数
strtok不保证线程安全代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <pthread.h>
void *fun(void *arg)
{
char buff[] = "1 2 3 4 5 6 7 8"; // fun的栈区
char *p = strtok(buff, " "); // 不保证线程安全
while(p != NULL)
{
printf("fun : %s\n", p);
p = strtok(NULL, " "); // 不保证线程安全
sleep(1);
}
}
int main()
{
pthread_t id;
int res = pthread_create(&id, NULL, fun, NULL);
assert(res == 0);
// 并发执行的代码
char buff[] = "a b c d e f g h"; // main线程的栈区
char *p = strtok(buff, " "); // 不保证线程安全
while(p != NULL)
{
printf("main: %s\n", p);
p = strtok(NULL, " "); // 不保证线程安全
sleep(1);
}
pthread_join(id, NULL);
exit(0);
}
执行结果:
主线程main本来是切割"abcdefg"的,但过一会也跟着函数线程fun去切割"12345678"了
把strtok换成strtok_r,保证线程安全代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <pthread.h>
void *fun(void *arg)
{
char buff[] = "1 2 3 4 5 6 7 8"; // fun的栈区
//char *p = strtok(buff, " "); // 不保证线程安全
char *q = NULL;
char *p = strtok_r(buff, " ", &q);
while(p != NULL)
{
printf("fun : %s\n", p);
// p = strtok(NULL, " "); // 不保证线程安全
p = strtok_r(NULL, " ", &q);
sleep(1);
}
}
int main()
{
pthread_t id;
int res = pthread_create(&id, NULL, fun, NULL);
assert(res == 0);
// 并发执行的代码
char buff[] = "a b c d e f g h"; // main线程的栈区
//char *p = strtok(buff, " "); // 不保证线程安全
char *q = NULL;
char *p = strtok_r(buff, " ", &q);
while(p != NULL)
{
printf("main: %s\n", p);
//p = strtok(NULL, " "); // 不保证线程安全
p = strtok_r(NULL, " ", &q);
sleep(1);
}
pthread_join(id, NULL);
exit(0);
}
执行结果如下:
主线程main始终切割"abcdefg",函数线程fun始终切割的是"12345678",因为并发执行所以顺序是不确定的,但是两个线程始终切割的内容没变
4.4 不保证线程安全的常见函数
5. 线程与fork
5.1 多线程中调用fork
多线程中某一个线程调用了fork创建子进程,在子进程中线程的运行情况是怎样的?
或者说:多线程中某个线程调用 fork(),子进程会有和父进程相同数量的线程吗?
对于如下代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <pthread.h>
void *fun(void *arg)
{
printf("fun start\n");
pid_t pid = fork();
assert(pid != -1);
if(pid == 0)
{
int i = 0;
for(; i < 3; ++i)
{
printf("child: mypid = %d\n", getpid());
sleep(1);
}
}
else
{
int i = 0;
for(; i < 3; ++i)
{
printf("father: mypid = %d\n", getpid());
sleep(1);
}
}
}
int main()
{
pthread_t id;
int res = pthread_create(&id, NULL, fun, NULL);//创建函数线程
assert(res == 0);
int i = 0;
for(; i < 5; ++i)
{
printf("main and mypid = %d\n", getpid());
sleep(1);
}
pthread_join(id, NULL);
exit(0);
}
执行结果如下:
所以:多线程中某个线程调用 fork(),子进程不会有和父进程相同数量的线程,子进程中只有调用fork的线程会被执行,其他线程不会被执行
5.2 多线程中调用fork的锁的继承问题
父进程被加锁的互斥锁 fork 后在子进程中是否也是已经加锁状态?
对于如下代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <pthread.h>
pthread_mutex_t mutex;
void *fun(void *arg)
{
sleep(1);
printf("fun start\n");
// pthread_mutex_lock(&mutex);
pid_t pid = fork(); // 调用fork时,互斥锁的状态是加锁状态
assert(pid != -1);
// pthread_mutex_unlock(&mutex); // 父子进程都会执行
if(pid == 0)//子进程
{
pthread_mutex_lock(&mutex);
int i = 0;
for(; i < 3; ++i)
{
printf("child: mypid = %d\n", getpid());
sleep(1);
}
pthread_mutex_unlock(&mutex);
}
else//父进程
{
pthread_mutex_lock(&mutex);
int i = 0;
for(; i < 3; ++i)
{
printf("father: mypid = %d\n", getpid());
sleep(1);
}
pthread_mutex_unlock(&mutex);
}
}
int main()
{
pthread_mutex_init(&mutex, NULL);
pthread_t id;
int res = pthread_create(&id, NULL, fun, NULL);
assert(res == 0);
pthread_mutex_lock(&mutex);
int i = 0;
for(; i < 5; ++i)
{
printf("main and mypid = %d\n", getpid());
sleep(1);
}
pthread_mutex_unlock(&mutex);
pthread_join(id, NULL);
pthread_mutex_destroy(&mutex);
exit(0);
}
执行结果:
可以看到,子进程并没有输出,而是被阻塞了,这是因为:调用fork时,子进程会复制父进程的锁(父子进程所使用的锁不是同一把锁),子进程会继承父进程的互斥锁的状态。而由于fork之后子进程中只有调用fork的线程会被执行,其他线程不会被执行,所以子进程不会执行main里面剩余的,即main刚开始加的锁在子进程中无法被解开,子进程就阻塞在了pthread_mutex_lock(&mutex);
这里;而对于父进程,在执行pthread_mutex_lock(&mutex);
时也会阻塞,但是等到父进程的主线程执行完毕,解锁之后,父进程的函数线程就可以继续输出了
那么如何解决这种情况呢?即如何使得fork出来的子进程能解开之前的锁同时保证线程安全呢?
解决方案:使用互斥锁对fork()调用做一个保护, 使得在fork调用的过程中锁是被自己加锁的,即在fork之前执行加锁,解锁操作写在fork下面,这样父子进程都会执行解锁操作。线程库提供了一个注册方法pthread_atfork()来做这件事情
int pthread_atfork(void (*prepare)(), void (*parent)(), void (*child)());
// 注册方法
/*
prepare这个方法 -- 对所有的锁执行加锁操作,它会在fork调用之前调用
parent这个方法 -- 对所有的锁执行解锁操作,它会在fork调用之后在父进程中执行
child这个方法 -- 对所有的锁执行解锁操作,它会在fork调用之后在子进程中执行
*/
pthread_atfork在fork()之前调用,当调用fork时,内部创建子进程前在父进程中会调用prepare,内部创建子进程成功后,父进程会调用parent ,子进程会调用child
添加如下三个函数到上例代码中:
void prepare()//对所有的锁执行加锁操作,它会在fork调用之前调用
{
pthread_mutex_lock(&mutex);
}
void parent()//对所有的锁执行解锁操作,它会在fork调用之后在父进程中执行
{
pthread_mutex_unlock(&mutex);
}
void child()//对所有的锁执行解锁操作,它会在fork调用之后在子进程中执行
{
pthread_mutex_unlock(&mutex);
}
在fork之前调用pthread_atfork方法:
pthread_atfork(prepare, parent, child);
执行结果如下:
子进程如愿输出