关于本篇博客的更多代码: GitHub链接
线程的同步与互斥
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。多个线程并发的操作共享变量,会带来⼀些问题。
- 线程共享线程所处的进程的虚拟地址空间,这些公共资源缺乏数据的访问控制容易造成数据混乱。
线程的同步:保证多线程访问公共资源的时序性。
线程的互斥:保证同一时间同一资源被多线程访问的唯一性问题。
生产者与消费者模型
一个场所,二个角色,三个关系:生产者与消费者争抢同一个临界区的临界资源, 生产者与生产者都在抢着访问操作同一个资源,生产者与生产者要保证互斥关系;消费者与消费者要保证互斥关系;生产者和消费者要保证同步与互斥关系。
为了保证维持生产者与消费者之间的关系来解决数据的安全访问操作,因此提出了同步与互斥,同步就是解决时序访问问题;互斥就是解决同一资源同一时间的唯一访问性问题。解决线程的安全问题实际就是映射模型中的关系来解决。
同步原则保证了不会产生饥饿问题,互斥原则保证了访问原子性。锁变量本身必须是原子操作。
互斥锁
1、 定义一个互斥锁
pthread_mutex_t mutex;
2、初始化互斥锁
- 定义时候直接赋值初始化,最后不需要手动释放
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- 函数接口初始化,最后需要手动释放
int pthread_mutex_init(pthread_mutex_t* mutex,const pthread_mutexattr_t* attr);
- mutex:互斥锁变量
attr:互斥锁属性,可以置空NULL
成功返回:0 错误:errno
3、对临界资源操作----需要进行加锁/解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
- 阻塞加锁,如果获取不到锁则阻塞等待锁被解开
int pthread_mutex_trylock(pthread_mutex_t *mutex);
- 非阻塞加锁,如果获取不到锁则立即报错返回EBUSY
int pthread_mutex_timedlock (pthread_mutex_t *mutex,struct timespec *t);
- 限时阻塞加锁,如果获取不到锁则等待指定时间,在这段时间内如果一直获取不到,则报错返回,否则加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
- 解锁
4、释放互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
- 实现线程间的同步:条件变量、posix信号量
/*这是一个火车站黄牛买票的栗子
* 每一个黄牛都是一个线程,在这个栗子中有一个总票数ticket
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>
int ticket = 100;
pthread_mutex_t mutex;//定义一个锁变量
void* sale_ticket(void* arg)
{
int id = (int)arg;
while(1){
pthread_mutex_lock(&mutex);//加锁
if(ticket > 0){
usleep(100);
printf("Yellow cow %d get a ticket ticket:%d\n",id,ticket);
ticket--;
}else{
printf("have no ticket:%d\n",ticket);
pthread_mutex_unlock(&mutex);//有可能退出,需要解锁,否则会死锁
pthread_exit(NULL);
}
pthread_mutex_unlock(&mutex);//解锁
}
return NULL;
}
int main()
{
pthread_t tid[4];
int i = 0;
pthread_mutex_init(&mutex,NULL);//使用函数初始化锁
for(i=0;i<4;i++)
{
int ret=pthread_create(&tid[i],NULL,sale_ticket,(void*)i);
//不能传i的地址,如果传i的地址,线程函数在调用这个地址时候都是3
if( ret !=0){
perror("pthread_create error");
exit(-1);
}
}
pthread_join(tid[0],NULL);
pthread_join(tid[1],NULL);
pthread_join(tid[2],NULL);
pthread_join(tid[3],NULL);
pthread_mutex_destroy(&mutex);//销毁锁
return 0;
}
对互斥锁进行操作的时候,有加锁就一定要有解锁,并且必须在任意一个有可能会退出的地方都要进行解锁操作,否则会造成其它线程的死锁。死锁情况:因为一直获取不到锁资源而造成的锁死情况
死锁的必要条件:必须具备条件才能满足
- 互斥条件-----我获取了锁你就不能再获取
- 不可剥夺条件----我拿到了锁别人不能释放我的锁
- 请求与保持条件----拿了锁1又去获取锁2,如果没有获取到锁2不释放锁1
- 环路等待条件----a拿了锁1去请求锁2,b拿了锁2去求锁1
预防产生死锁:破坏请求与保持条件。 也就要保证,一个线程在拥有一个锁之后,在这个互斥锁解锁之前,不去拿第二个锁。
避免产生死锁的经典实例:银行家算法
4、读写者模型–读写锁
适用场景:在编写多线程的时候,有⼀种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。 通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有⼀种⽅法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。读写锁本质上是⼀种⾃旋锁
注意:写独占,读共享,写锁优先级⾼
读写锁就是基于自旋锁实现的
自旋锁是一直轮询判断,非常消耗CPU资源,是用于确定等待花费时间比较少,很快就能获取到锁的这种情况。互斥锁是挂起等待。
/* 这是一个验证读写锁的代码
* 1. 读写锁的初始化
* 2. 读写锁的操作(加读锁/加写锁/解锁)
* 3. 读写锁的释放
* 特性:
* 写独占,读共享,写优先级高
* 有多个写线程,多个读线程,验证特性
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <pthread.h>
int ticket = 100;
pthread_rwlock_t rwlock;
void *thr_write(void *arg)
{
while(1) {
//加写锁
//int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
//int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
//pthread_rwlock_wrlock(&rwlock);//写独占
//pthread_rwlock_rdlock(&rwlock);//读共享
pthread_rwlock_wrlock(&rwlock);//写独占
if (ticket > 0 ) {
sleep(5);
ticket--;
printf("ticket:%d\n", ticket);
}
printf("this is write!!\n");
pthread_rwlock_unlock(&rwlock);
sleep(5);
}
return 0;
}
void *thr_read(void *arg)
{
while(1) {
pthread_rwlock_rdlock(&rwlock);
if (ticket > 0) {
sleep(5);
ticket--;
printf("ticket:%d\n", ticket);
}
printf("this is read!!!\n");
pthread_rwlock_unlock(&rwlock);
sleep(5);
}
return 0;
}
int main()
{
pthread_t wtid[4], rtid[4];
int ret, i;
//1. 读写锁的初始化
//int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
// const pthread_rwlockattr_t *restrict attr);
pthread_rwlock_init(&rwlock, NULL);
for (i = 0; i < 4; i++) {
ret = pthread_create(&wtid[i], NULL, thr_write, NULL);
if (ret != 0) {
printf("pthread_create error\n");
return -1;
}
}
for (i = 0; i < 4; i++) {
ret = pthread_create(&rtid[i], NULL, thr_read, NULL);
if (ret != 0) {
printf("pthread_create error\n");
return -1;
}
}
pthread_join(wtid[0], NULL);
pthread_join(wtid[1], NULL);
pthread_join(wtid[2], NULL);
pthread_join(wtid[3], NULL);
pthread_join(rtid[0], NULL);
pthread_join(rtid[1], NULL);
pthread_join(rtid[2], NULL);
pthread_join(rtid[3], NULL);
//3. 销毁读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
实际中使用互斥锁(阻塞等待)还是读写锁(自旋锁)取决于正在占用锁的线程占用锁的时间。