目录
线程的安全
线程共享地址空间,因此通信变得非常方便,但因为有数据有可能存在争抢关系,所以数据的安全访问变得特别重要。线程间在操作临界资源时需要保证数据的同步与互斥保证数据安全访问
线程的同步与互斥
一个进程中可能会有很多线程,然而这些线程共享同一个虚拟地址,线程间通信变得简单了,但是数据安全访问问题变得突出了(因为大家都在争抢访问一些公共资源)。
其实在资源争抢这里我们用了一个模型(生产消费者模型)来体现了如何能够让这些访问资源的变得安全
同步与互斥方式
互斥锁,条件变量,信号量,读写锁,自旋锁
互斥:保证一个公共资源在同一时间的唯一访问性。(线程或进程间对临界资源在同一时间的唯一访问性)
线程间的互斥实现:互斥锁(互斥量),条件变量,posix信号量
同步:时序的制约访问(线程或进程间对临界资源的顺序访问关系)
线程间的同步实现:条件变量,posix信号量
生产者与消费者模型:
(一个场所(峰值缓冲),两个角色,三个关系)
生产者与生产者之间的关系:
都在抢着访问操作同一个资源,如果要实现安全访问那么需要保证一个互斥的关系才可以
(生产者与生产者之间需要一个互斥关系,来保证数据的安全操作)
生产者与消费者之间的关系:
只有生产出来之后才能消费,这里讲究一个时序制约,所以需要保证一个同步与互斥的关系才可以实现数据安全访问。
(需要有一个同步关系与互斥关系,来保证数据的安全操作)
消费者与消费者的关系:
都在抢着访问操作同一个资源,如果要实现安全访问那么需要保证一个互斥关系才行
(消费者与消费者之间也需要一种互斥的关系,来保证数据的安全操作)
mutex(互斥量)
多线程并发操作关系变量,会带来一些问题
代码演示
//这是一个火车站黄牛买票的例子
//每一个黄牛都是一个线程,例子中有总票数固定
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
int ticket = 10 ;
void *cow(void *arg)
{
int id = (int)arg;
while(1)
{
if(ticket > 0)
{
sleep(1);
printf("cow:%d get a ticket:%d\n",id,ticket);
ticket--;
}
else
{
printf("have no ticker\n");
pthread_exit(NULL);
}
}
}
int main()
{
pthread_t tip[4];
int i = 0;
int ret;
for(i = 0; i < 4;i++)
{
ret = pthread_create(&tip[i],NULL,cow,(void*)i);
if(ret != 0)
{
printf("pthread_create error\n");
return -1;
}
}
pthread_join(tip[0],NULL);
pthread_join(tip[1],NULL);
pthread_join(tip[2],NULL);
pthread_join(tip[3],NULL);
return 0;
}
、
线程如果没有实现互斥,争抢资源会造成问题
实现一个互斥操作(同时唯一访问)
- 定义一个互斥锁
互斥锁的初始化 有两种方式:
1:定义时直接赋值初始化,最后不需要手动释放
2:函数接口初始化,最后需要动手释放#include <pthread.h> int pthread_mutex_destroy(pthread_mutex_t *mutex); int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- 初始化互斥锁
- 对临界操作进行加锁或解锁
在线程创建之前加锁或者解锁 - 释放互斥锁
在任意有可能的退出的地方都要释放
//这是一个火车站黄牛买票的例子
//每一个黄牛都是一个线程,例子中有总票数固定
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
pthread_mutex_t mutex;
int ticket = 10 ;
void *cow(void *arg)
{
int id = (int)arg;
while(1)
{
// int pthread_mutex_lock(pthread_mutex_t *mutex);
// 阻塞加锁,如果获取不到锁则阻塞等待直到锁被解开
// int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 非阻塞加锁,如果获取不到锁则立刻报错返回EBUSY
// int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
// const struct timespec *restrict abs_timeout);
// 限时阻塞加锁,如果获取不到锁则等待指定时间,
// 在这段时间如果一直获取不到,则报错返回,否则加锁
pthread_mutex_lock(&mutex);
if(ticket > 0)
{
sleep(1);
printf("cow:%d get a ticket:%d\n",id,ticket);
ticket--;
}
else
{
printf("have no ticker\n");
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);//任何可能退出的地方都要解锁
//否则会导致其他线程阻塞卡死
}
// int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 解锁
pthread_mutex_unlock(&mutex);
}
}
int main()
{
pthread_t tip[4];
int i = 0;
int ret;
// int pthread_mutex_init(pthread_mutex_t *restrict mutex,
// const pthread_mutexattr_t *restrict attr);
//互斥锁的初始化
//mutex:互斥锁变量
//attr:互斥锁的属性:NULL
//返回值:0成功,errno错误
pthread_mutex_init(&mutex,NULL);
for(i = 0; i < 4;i++)
{
ret = pthread_create(&tip[i],NULL,cow,(void*)i);
if(ret != 0)
{
printf("pthread_create error\n");
return -1;
}
}
pthread_join(tip[0],NULL);
pthread_join(tip[1],NULL);
pthread_join(tip[2],NULL);
pthread_join(tip[3],NULL);
//释放
// #include <pthread.h>
// int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_mutex_destroy(&mutex);
return 0;
}
死锁
因为一直获取不到锁资源而造成的锁死情况
死锁产生的必要条件:必需具备条件
- 互斥条件 ——我获取了你就不能获取
- 不可剥夺条件: ——我拿到了锁,别人不能释放我锁
- 请求与保持条件:——拿锁1之后又去拿锁2,没有拿到锁2不释放锁1(卡在获取锁2)
- 环路等待条件: ——a拿了锁1去请求锁2,b拿了锁2去请求锁1
满足这四个条件就会出现死锁。
如何预防产生死锁:破坏死锁产生的必要条件
避免产生死锁:银行家算法
- 安全状态
- 非安全状态
锁编号:大家都按照这个编号来获取锁,以及释放
限时等待(timelock)
条件变量
初始化
条件变量的初始化有两种方式
1:定义赋值初始化,不需要释放
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
2:函数接口初始化,需要释放
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
cond:要在这个条件变量上等待
mutex:互斥量
唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond);
唤醒所有等待在条件变量上的线程
int pthread_cond_signal(pthread_cond_t *cond);
唤醒第一个等待在条件变量上的线程
销毁
int pthread_cond_destroy(pthread_cond_t *cond);
互斥锁和条件变量搭配使用,是先对互斥做了一个判断是否加锁,如果加了锁就解锁,然后陷入等待(整个过程是原子操作)
要防止的情况是:假如没有面,但是消费者又速度比较快,先拿到锁了,那么生产者将拿不到锁,没法产生将会造成双方卡死
所以如果消费者先获取到锁,那么在陷入等待之前需要解锁
而这里的锁的存在是为了保护这个全局的条件的操作是受保护的
为什么条件变量要和互斥锁一起使用(为什么pthread_cond_wait需要互斥量)?
因为等待需要被唤醒,而被唤醒的前提条件是条件被满足。并且这个条件本身就是一个临界资源,因此改变条件的操作
需要被保护。
- 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等待都不会满足,所以必须有一个线程通过某些操作,改变共享变量,使原先不满足的条件满足,并且通知等待在条件变量上的线程
- 条件满足必然会牵涉到共享数据的变化,所以一定要用互斥锁来保护,没有互斥锁就无法安全的获取和修改共享数据
- 解锁和等待必须是原子操作,否则解锁之后,等待之前如果其他线程获取到互斥量,摒弃条件满足,发送了信号
那么pthread_cond_wait将错过这个信号,会导致永远阻塞在这里 - int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
进入该函数之后,会看条件变量等于0?等于,就把互斥量变成1,直到wait返回,条件量
改为1,互斥量恢复原样
条件变量使用规范
1:等待条件代码
2:给条件发送信号
posix信号量
posix和system v信号量作用相同,都是同步操作,达到访问关系资源无冲突的目的
但是posix既可以用于实现进程间的同步与互斥,也可以用于线程间的同步与互斥
信号量是什么?与条件变量的区别
具有一个等待队列的计数器,信号量修改的是直接内部的资源计数,这个内部的资源计数就是条件,
而条件变量修改的是外部资源,需要我们用户来修改
同步实现:
消费者:没有资源则等待
生产者:生产出来资源则等待通知等待队列中的等待者
初始化信号量
头文件: #include <semaphore.h>
链接命令: Link with -lrt or -pthread.
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:pshared:0表示线程间共享,非零表示进程间共享
等待信号量
int sem_wait(sem_t *sem);阻塞等待
int sem_trywait(sem_t *sem);非阻塞等待
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);限时等待
销毁信号量
int sem_destroy(sem_t *sem);
发布信号量
int sem_post(sem_t *sem);
功能:发布信号量,表示资源使用完毕,可以归还资源了,将信号值加1
代码演示
//这是使用信号量实现线程间同步与互斥的代码
//1:信号量初始化
//2:信号量的操作(等待与通知)
//3:信号量的释放
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
#include<string.h>
#include<errno.h>
#include<semaphore.h>
sem_t sem;
void *producer(void *arg)
{
while(1)
{
sleep(1);
//没有资源则等待
printf("生产者\n");
sem_post(&sem);//生产出资源后通知等待在信号量上的进程或线程
}
}
void *consumer(void *arg)
{
while(1)
{
sem_wait(&sem);
printf("消费者\n");
}
}
//线程间的同步与互斥
int main()
{
pthread_t tid1,tid2;
int ret;
//初始化信号量
//int sem_init(sem_t *sem, int pshared, unsigned int value);
//sem:信号量
//pshared:
// 0:用于线程间
// 非0:用于进程间
//value:信号量的初始化计数
ret = sem_init(&sem,0,0);
if(ret < 0)
{
perror("sem_init");
return -1;
}
ret = pthread_create(&tid1,NULL,producer,NULL);
if(ret != 0)
{
perror("pthread_create");
return -1;
}
ret = pthread_create(&tid2,NULL,consumer,NULL);
if(ret != 0)
{
perror("pthread_create");
return -1;
}
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
//销毁信号量
//int sem_destroy(sem_t *sem);
sem_destroy(&sem);
return 0;
}
读写者模型:
少量写
大量读
写独占,读共享,写锁优先级高
写的时候不能读,读的时候不能写,写的时候别人不能写,读的时候都可以读
读写互斥,写与写之间互斥,读与读之间不冲突。
(写优先:因为读的时候不能写,如果读的多的话,有可能就一直没法写,因此写优先指的是,一般说要开始写,会等待当前正在读的人读完,但是还有人在通知了准备开始写之后想要读,则不能读,直到写完)
读写锁
在编写多线程的时候,有一种情况就是有些公共数据的修改机会比较少,相比较改写
它们的读机会反而高的多,在读的过程中,往往伴随着查找的操作,中间操作时间很长,
给这种代码段加锁,会极大地降低我们效率,所以就有了读写锁
当前锁状态 | 读锁请求 | 写锁请求 |
无锁 | 可以 | 可以 |
读锁 | 可以 | 阻塞 |
写锁 | 阻塞 | 阻塞 |
注意:写独占,读共享,写锁优先级高
读写锁接口
初始化和销毁
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
解锁和加锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
自旋锁:
互斥锁是挂起等待
自旋锁是一直在轮询判断,非常消耗cpu资源(适用于确定等待花费时间比较少,很快就能获取到锁的这种情况)