问题引入:
例子:两条线程同时访问某一片内存空间导致数据践踏。
#include<stdio.h>
#include <pthread.h>
int g_val = 0;
//线程的例程函数,也就是创建线程之后,去执行这个函数
void* routine1(void *arg)
{
g_val = 100;
sleep(1);//延时1秒,此时线程2就执行,g_val = 200;
printf("routine1 100 g_val:%d\n",g_val);
}
//线程的例程函数,也就是创建线程之后,去执行这个函数
void* routine2(void *arg)
{
sleep(1);//延时1秒,让线程1先开始执行,此时 g_val = 100;
g_val = 200;
printf("routine2 200 g_val:%d\n",g_val);
}
int main()
{
// 创建一个新的线程1
pthread_t thread1;
pthread_create(&thread1,NULL,routine1,NULL);
// 创建一个新的线程2
pthread_t thread2;
pthread_create(&thread2,NULL,routine2,NULL);
//接合子线程 --阻塞等待子线程退出 回收资源
pthread_join(thread1,NULL);
pthread_join(thread2,NULL);
return 0;
}
结果:
如何去避免 不同线程之间 访问同一个资源 导致 的数据践踏问题呢??
也就是说,线程1在访问这个资源的时候,别的线程不能访问,要等线程1访问结束之后,别的线程才能访问。 -----使用线程同步互斥机制。
一、线程同步互斥
1、什么是同步互斥呢? 为什么要处理同步互斥?
同步互斥 就是使得线程处理任务时有先后顺序,为了防止线程资源被抢占的问题。
2、处理同步互斥方式有哪些?
信号量 ------》进程
有名信号量 ---》进程
无名信号量 ---》线程
互斥锁 ------》线程
读写锁 -------》线程
二、同步互斥方式之一-----有名信号量
1、什么是有名信号量
有名信号量 跟 信号量非常相似,信号量的值只能是 0/1,但是有名信号量的值可以是 0~正无穷。
信号量 使用了 空间 + 数据 ,有名信号量 只是使用了数据来处理。
2、有名信号量的函数接口
1)创建 并且打开一个有名信号量 ?? --sem_open
NAME
sem_open - initialize and open a named semaphore
//初始化 和 打开 一个有名信号量
SYNOPSIS
#include <fcntl.h> /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag,mode_t mode, unsigned int value);
参数:
name:有名信号量的名字 ,要求必须以 "/"开头,比如 "/sem_test" ,存在于 /dev/shm
oflag:
O_CREAT --->不存在就创建
O_EXCL ----》存在了就报错
mode:八进制权限 0777 0666
value: 有名信号量的初始值
注意: 如果有 oflag中有 O_CREAT选项,则参数 mode 和 value 必须要填写。
如果有名信号量存在了,但是你又写了O_CREAT,那么你后面填写的mode和value就不会起作用。
返回值:
成功返回 有名信号量的地址
失败 SEM_FAILED
2)有名信号量的P操作 ---》sem_wait
p操作: 资源数 -1 操作
#include <semaphore.h>
int sem_wait(sem_t *sem);
参数:
sem:有名信号量的地址
返回值:
成功返回 0
失败返回 -1
如果 当前的信号量的值 是 2 ,那么 sem_wait 就会马上返回,这个值就变成了 1
如果 当前的信号量的值 是 1 ,那么 sem_wait 就会马上返回,这个值就变成了 0
如果 当前的信号量的值 是 0 ,那么 sem_wait 就会阻塞等待,一直阻塞到有名信号量的值 >0的时候
3)有名信号量的V操作 ---》sem_post
V操作:资源数 +1 操作 ---》一定是可以+1的,不会阻塞
#include <semaphore.h>
int sem_post(sem_t *sem);
参数:
sem:有名信号量的地址
返回值:
成功返回 0
失败返回 -1
4)关闭有名信号量。---》sem_close
#include <semaphore.h>
int sem_close(sem_t *sem);
参数:
sem:有名信号量的地址
返回值:
成功返回 0
失败返回 -1
5)删除有名信号量 ---》sem_unlink
NAME
sem_unlink - remove a named semaphore
//删除有名信号量
SYNOPSIS
#include <semaphore.h>
int sem_unlink(const char *name);
参数:
name:有名信号量的名字 sem_test
返回值:
成功返回 0
失败返回 -1
3、练习
将之前 的 信号量 + 共享内存两个进程的通信 -----》变成 有名信号量 + 共享内存
07使用共享内存+信号量实现通信1.c ----》使用共享内存+有名信号量实现通信1.c
08使用共享内存+信号量实现通信2.c ----》使用共享内存+有名信号量实现通信2.c
//02使用共享内存+有名信号量实现通信1.c
#include<stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <fcntl.h> /* For O_* constants */
#include <sys/stat.h>
#define SEM_NAME "/sem_test"
int main()
{
//1)先获取 key值。
key_t key = ftok(".",10);
//2)根据key值 获取 共享内存的ID号,如果说key值对应的共享内存不存在,则创建
int shmid = shmget(key,1024,IPC_CREAT|0666);
//3)根据ID号将共享内存 映射 到 本进程虚拟内存空间的某个区域
char*shm_p = shmat(shmid, NULL, 0);
//使用有名信号量 协调 两个进程之间通信
//1)创建 并且打开一个有名信号量
sem_t *sem = sem_open(SEM_NAME, O_CREAT,0777, 0);
//int count = 0;
while(1)
{
scanf("%s",shm_p);
//把车开进去,有车了 数据+1 V操作 --》sem_post
sem_post(sem);
//退出的时候 byebye
if(strncmp(shm_p,"byebye",6) == 0)
{
sleep(1);
break;
}
//printf("count:%d\n",count++);
}
//最后不用的时候,解除映射
shmdt(shm_p);
//5)当没有进程再需要使用这一块共享内存时,删除释放它
shmctl(shmid,IPC_RMID, NULL);
//4)关闭有名信号量。---》sem_close
sem_close(sem);
//5)删除有名信号量 ---》sem_unlink
sem_unlink(SEM_NAME);
return 0;
}
//03使用共享内存+有名信号量实现通信2.c
#include<stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <fcntl.h> /* For O_* constants */
#include <sys/stat.h>
#define SEM_NAME "/sem_test"
int main()
{
//1)先获取 key值。
key_t key = ftok(".",10);
//2)根据key值 获取 共享内存的ID号,如果说key值对应的共享内存不存在,则创建
int shmid = shmget(key,1024,IPC_CREAT|0666);
//3)根据ID号将共享内存 映射 到 本进程虚拟内存空间的某个区域
char*shm_p = shmat(shmid, NULL, 0);
//使用有名信号量 协调 两个进程之间通信
//1)创建 并且打开一个有名信号量
sem_t *sem = sem_open(SEM_NAME, O_CREAT,0777, 0);
while(1)
{
//把车提出来, 数据 -1 P操作
//如果没车,也就是数据为0 ,不能进行P操作 此时该函数就会阻塞
sem_wait(sem);
printf("%s\n",shm_p);
//退出的时候 byebye
if(strncmp(shm_p,"byebye",6) == 0)
break;
}
//最后不用的时候,解除映射
shmdt(shm_p);
return 0;
}
三、无名信号量 ----线程同步互斥
一般作用于 线程之间的互斥,由于是无名信号量,所以说是没有名字的,不能使用 sem_open 打开。
1、无名信号量的函数接口
1) 定义一个无名信号量 (数据类型 sem_t)
sem_t sem; ---无名信号量 --》变量
2) 初始化无名信号量 ---》 man 3 sem_init
NAME
sem_init - initialize an unnamed semaphore
SYNOPSIS
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
sem --》无名信号量的变量的地址
pshared---》
0 --》作用于 线程之间 ---只考虑这种情况
非0 --》作用于 进程之间
value --》无名信号量 初始化的值
返回值:
成功返回 0
失败返回 -1
3) 无名信号量的P操作 ---》sem_wait
p操作: 资源数 -1 操作
#include <semaphore.h>
int sem_wait(sem_t *sem);
参数:
sem:无名信号量的变量的地址
返回值:
成功返回 0
失败返回 -1
如果 当前的信号量的值 是 2 ,那么 sem_wait 就会马上返回,这个值就变成了 1
如果 当前的信号量的值 是 1 ,那么 sem_wait 就会马上返回,这个值就变成了 0
如果 当前的信号量的值 是 0 ,那么 sem_wait 就会阻塞等待,一直阻塞到有名信号量的值 >0的时候
4)无名信号量的V操作 ---》sem_post
V操作:资源数 +1 操作 ---》一定是可以+1的,不会阻塞
#include <semaphore.h>
int sem_post(sem_t *sem);
参数:
sem:无名信号量的变量的地址
返回值:
成功返回 0
失败返回 -1
5)销毁 无名信号量 ----》man 3 sem_destroy
sem_destroy - destroy an unnamed semaphore
SYNOPSIS
#include <semaphore.h>
int sem_destroy(sem_t *sem);
参数:
sem:无名信号量的变量的地址
返回值:
成功返回 0
失败返回 -1
2、练习
把 两个线程 访问 同一个资源 发生 数据 践踏的问题 使用 无名信号量 进行 解决。
也就是说, 线程 1 在 使用 g_val变量的时候,也就是访问该变量所在的内存空间的时候 ,别的线程 都不可以访问 。
#include<stdio.h>
#include<pthread.h>
#include<semaphore.h>
int g_val = 0;
//1) 定义一个无名信号量 (数据类型 sem_t)
sem_t sem;
//线程的例程函数,也就是创建线程之后,去执行这个函数
void* routine1(void *arg)
{
//无名信号量P操作 资源数 -1
//如果当前资源数为 0 此时不能减1,该函数会阻塞等待资源数
sem_wait(&sem);
g_val = 100;
sleep(1);//延时1秒,此时线程2就执行,g_val = 200;
printf("routine1 100 g_val:%d\n",g_val);
//无名信号量 V操作 资源数+1
sem_post(&sem);
}
//线程的例程函数,也就是创建线程之后,去执行这个函数
void* routine2(void *arg)
{
//无名信号量P操作 资源数 -1
//如果当前资源数为 0 此时不能减1,该函数会阻塞等待资源数
sem_wait(&sem);
sleep(1);//延时1秒,让线程1先开始执行,此时 g_val = 100;
g_val = 200;
printf("routine2 200 g_val:%d\n",g_val);
//无名信号量 V操作 资源数+1
sem_post(&sem);
}
int main()
{
//2) 初始化无名信号量
sem_init(&sem, 0,1);
// 创建一个新的线程1
pthread_t thread1;
pthread_create(&thread1,NULL,routine1,NULL);
// 创建一个新的线程2
pthread_t thread2;
pthread_create(&thread2,NULL,routine2,NULL);
//接合子线程 --阻塞等待子线程退出 回收资源
pthread_join(thread1,NULL);
pthread_join(thread2,NULL);
//销毁 无名信号量
sem_destroy(&sem);
return 0;
}
四、线程互斥方式之一----互斥锁
1、什么是互斥锁
互斥锁 是专门 用于处理线程互斥的一种方式,它有两种状态: 上锁状态/解锁状态。
特点:如果互斥锁处于上锁状态,那么再上锁就会造成阻塞,直到这把锁解开之后,才能上锁。
解锁状态依然可以解锁,不会阻塞。
2、关于线程互斥锁的函数接口
1)定义互斥锁变量。 -----》数据类型 pthread_mutex_t
pthread_mutex_t mutex;
2)初始化 互斥锁 ----pthread_mutex_init
NAME
initialize a mutex
SYNOPSIS
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t * mutex,const pthread_mutexattr_t * attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//静态初始化
参数:
mutex:互斥锁变量的地址
attr: 互斥锁的属性, 默认属性 为NULL
3)上锁 ----》pthread_mutex_lock
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
参数:
mutex:互斥锁变量的地址
返回值:
成功返回 0
失败返回 错误码
4)解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数:
mutex:互斥锁变量的地址
返回值:
成功返回 0
失败返回 错误码
5)销毁互斥锁 ---》pthread_mutex_destroy
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:
mutex:互斥锁变量的地址
返回值:
成功返回 0
失败返回 错误码
3、互斥锁的使用场景
当我们使用一些临界资源时,防止多个线程同时访问,我们可以这么做,在线程访问临界资源前,让当前这个线程先上锁,
然后再访问资源,访问完之后就解锁,让别的线程去上锁。
4、练习
将 04线程使用无名信号量实现同步互斥.c ----->改成 05线程使用互斥锁实现同步互斥.c
//05线程使用互斥锁实现同步互斥.c
#include<stdio.h>
#include<pthread.h>
#include<semaphore.h>
int g_val = 0;
//1)定义互斥锁变量。 -----》数据类型 pthread_mutex_t
pthread_mutex_t mutex;
//线程的例程函数,也就是创建线程之后,去执行这个函数
void* routine1(void *arg)
{
pthread_mutex_lock(&mutex);//上锁
g_val = 100;
sleep(1);//延时1秒,此时线程2就执行,g_val = 200;
printf("routine1 100 g_val:%d\n",g_val);
pthread_mutex_unlock(&mutex);//解锁
}
//线程的例程函数,也就是创建线程之后,去执行这个函数
void* routine2(void *arg)
{
pthread_mutex_lock(&mutex);//上锁
sleep(1);//延时1秒,让线程1先开始执行,此时 g_val = 100;
g_val = 200;
printf("routine2 200 g_val:%d\n",g_val);
pthread_mutex_unlock(&mutex);//解锁
}
int main()
{
//2)初始化 互斥锁
pthread_mutex_init(&mutex,NULL);
// 创建一个新的线程1
pthread_t thread1;
pthread_create(&thread1,NULL,routine1,NULL);
// 创建一个新的线程2
pthread_t thread2;
pthread_create(&thread2,NULL,routine2,NULL);
//接合子线程 --阻塞等待子线程退出 回收资源
pthread_join(thread1,NULL);
pthread_join(thread2,NULL);
//5)销毁互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
//06线程使用互斥锁实现同步互斥.c
#include<stdio.h>
#include<pthread.h>
#include<semaphore.h>
int g_val = 0;
//1)定义互斥锁变量。 -----》数据类型 pthread_mutex_t
pthread_mutex_t mutex;
//线程的例程函数,也就是创建线程之后,去执行这个函数
void* routine1(void *arg)
{
pthread_mutex_lock(&mutex);//上锁
g_val = 100;
int i;
//如果当前线程1 已经 上锁,那么在线程1 跑的 这 20S之内 ,主线程 发送了 取消请求给它 ,那么这个时候线程1取消了
//记得,在子线程退出之前(响应 取消请求 之前,先把锁解开)
for(i=0; i<20; i++)
{
sleep(1);
printf("%d routine1 100 g_val:%d\n",i,g_val);
}
pthread_mutex_unlock(&mutex);//解锁
}
//线程的例程函数,也就是创建线程之后,去执行这个函数
void* routine2(void *arg)
{
sleep(1);//强行延时1S,让上面的子线程1 先跑
pthread_mutex_lock(&mutex);//上锁
g_val = 200;
int i;
for(i=0; i<20; i++)
{
sleep(1);
printf("routine2 200 g_val:%d\n",g_val);
}
pthread_mutex_unlock(&mutex);//解锁
}
//线程的例程函数,也就是创建线程之后,去执行这个函数
void* routine3(void *arg)
{
//这里我是没有上锁的
int i;
for(i=0; i<5; i++)
{
sleep(1);
printf("routine3\n");
}
}
int main()
{
//2)初始化 互斥锁
pthread_mutex_init(&mutex,NULL);
// 创建一个新的线程1
pthread_t thread1;
pthread_create(&thread1,NULL,routine1,NULL);
// 创建一个新的线程2
pthread_t thread2;
pthread_create(&thread2,NULL,routine2,NULL);
// 创建一个新的线程3
//pthread_t thread3;
//pthread_create(&thread3,NULL,routine3,NULL);
sleep(5);
//发送取消请求给 子线程1
pthread_cancel(thread1);
//接合子线程 --阻塞等待子线程退出 回收资源
pthread_join(thread1,NULL);
pthread_join(thread2,NULL);
//pthread_join(thread3,NULL);
//5)销毁互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
五、线程的取消处理 例程 函数
#include <pthread.h>
void pthread_cleanup_push(void(*routine)(void *), void *arg);
函数作用:压栈的取消处理例程 ,也就是说,在线程 收到 取消请求的时候 ,让 线程先执行 取消例程 函数 routine
参数:
routine :线程的取消例程 函数 ,线程收到取消请求之后,会执行这个函数再 退出(也就是再响应 取消请求 )
arg: 给例程函数传递的参数
void pthread_cleanup_pop(int execute);
函数作用:弹栈的取消处理例程 ,也就是说,如果线程 没有收到 取消请求 ,那么也会根据 参数 execute 决定 是否 执行 取消例程 函数
参数:
execute---》 0 --》表示 该线程正常退出的时候, 不执行 例程函数
非0---》表示 该线程正常退出的时候, 也执行 例程函数
注意:上面两个函数是成对出现 ,配套使用
#include<stdio.h>
#include<pthread.h>
#include<semaphore.h>
int g_val = 0;
//1)定义互斥锁变量。 -----》数据类型 pthread_mutex_t
pthread_mutex_t mutex;
//线程的取消处理例程
//线程收到取消 请求的时候 ,先执行这个函数 ,再退出
void exitRoutine(void *arg)
{
printf("exitRoutine\n");
//解锁
pthread_mutex_unlock(&mutex);//解锁
}
//线程的例程函数,也就是创建线程之后,去执行这个函数
void* routine1(void *arg)
{
//先压栈 压栈的取消处理例程
pthread_cleanup_push(exitRoutine,NULL);
pthread_mutex_lock(&mutex);//上锁
g_val = 100;
int i;
//如果当前线程1 已经 上锁,那么在线程1 跑的 这 20S之内 ,主线程 发送了 取消请求给它 ,那么这个时候线程1取消了
//记得,在子线程退出之前(响应 取消请求 之前,先把锁解开)
for(i=0; i<20; i++)
{
sleep(1);
printf("%d routine1 100 g_val:%d\n",i,g_val);
}
pthread_mutex_unlock(&mutex);//解锁
//弹栈
pthread_cleanup_pop(0);
}
//线程的例程函数,也就是创建线程之后,去执行这个函数
void* routine2(void *arg)
{
sleep(1);//强行延时1S,让上面的子线程1 先跑
//先压栈 压栈的取消处理例程
pthread_cleanup_push(exitRoutine,NULL);
pthread_mutex_lock(&mutex);//上锁
g_val = 200;
int i;
for(i=0; i<20; i++)
{
sleep(1);
printf("routine2 200 g_val:%d\n",g_val);
}
pthread_mutex_unlock(&mutex);//解锁
//弹栈
pthread_cleanup_pop(0);
}
//线程的例程函数,也就是创建线程之后,去执行这个函数
void* routine3(void *arg)
{
//这里我是没有上锁的
int i;
for(i=0; i<5; i++)
{
sleep(1);
printf("routine3\n");
}
}
int main()
{
//2)初始化 互斥锁
pthread_mutex_init(&mutex,NULL);
// 创建一个新的线程1
pthread_t thread1;
pthread_create(&thread1,NULL,routine1,NULL);
// 创建一个新的线程2
pthread_t thread2;
pthread_create(&thread2,NULL,routine2,NULL);
// 创建一个新的线程3
//pthread_t thread3;
//pthread_create(&thread3,NULL,routine3,NULL);
sleep(5);
//发送取消请求给 子线程1
pthread_cancel(thread1);
//接合子线程 --阻塞等待子线程退出 回收资源
pthread_join(thread1,NULL);
pthread_join(thread2,NULL);
//pthread_join(thread3,NULL);
//5)销毁互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
六、线程 与 进程 的 区别
(1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
(2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行
(3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源.
(4)系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。