系统编程第六节——线程的同步互斥

一、线程的互斥

临界资源:系统中多个任务都可以访问的资源。

在多线程编程中,必须要注意对临界资源的保护。

问题:假设在一个办公室内,有一台打印机,这个打印机所有人都可以共享使用,但是在某一个人使用时,希望在这一段时间内只能被自己使用(互斥),如何实现这个功能?

解决方案:不管是谁使用打印机,在使用之前给打印机上锁,用完之后,解锁。

1、线程互斥锁

线程互斥锁解决的问题就是临界资源的独占式使用,当进程中的线程给某一个资源上锁之后,其他的线程想要使用这个资源的时候,必须等待原来的线程解锁之后才能使用这个资源。

如果一条线程想要对一个已经上锁的资源进行上锁,此时线程会进入阻塞,必须等待原来上锁的线程对这个资源解锁之后,才能成功上锁,解除阻塞。

2、相关API

1)互斥锁初始化 pthread_mutex_init()
2)上锁
3)解锁
4)互斥锁销毁

SYNOPSIS

   #include <pthread.h>

   pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;
   pthread_mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
   pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;

   int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);		//互斥锁初始化
   int pthread_mutex_lock(pthread_mutex_t *mutex);	//上锁
   int pthread_mutex_trylock(pthread_mutex_t *mutex);	//尝试上锁
   int pthread_mutex_unlock(pthread_mutex_t *mutex); //解锁
   int pthread_mutex_destroy(pthread_mutex_t *mutex); //互斥锁销毁

==> mutex : 互斥锁变量地址
==> mutexattr : 互斥锁属性变量地址 NULL

返回值: 成功返回0,失败返回错误编码

例子1:

创建两条线程,使用同一个临界资源打印机 (输出缓冲区),线程1输出字符串”hello”,每隔1s打印一个字符,线程2输出字符串”world”, 每隔1s打印1个字符。
没加互斥锁

#include <pthread.h>
#include <unistd.h>
#include <stdio.h>

void *fun1(void *arg)
{
	char *p = "hello";

	while(*p)
	{
		putc(*p,stderr);
		p++;
		sleep(1);
	}
	
}

void *fun2(void *arg)
{
	char *p = "world";

	while(*p)
	{
		putc(*p,stderr);
		p++;
		sleep(1);
	}

}

int main(int argc,char *argv[])
{


	//1,创建2条线程分别输出"hello","world"
	pthread_t tid[2];
	pthread_create(&tid[0],NULL,fun1,NULL);
	pthread_create(&tid[1],NULL,fun2,NULL);

	//2,等待两条子线程结束
	pthread_join(tid[0],NULL);
	pthread_join(tid[1],NULL);

	return 0;
}

在这里插入图片描述打印结果不是我们想要的

加互斥锁

#include <pthread.h>
#include <unistd.h>
#include <stdio.h>

//定义一个全局互斥锁变量
pthread_mutex_t mutex;

void *fun1(void *arg)
{
	char *p = "hello";
	pthread_mutex_lock(&mutex);

	while(*p)
	{
		putc(*p,stderr);
		p++;
		sleep(1);
	}
	pthread_mutex_unlock(&mutex); //用完打印机后
}

void *fun2(void *arg)
{
	char *p = "world";
	pthread_mutex_lock(&mutex);

	while(*p)
	{
		putc(*p,stderr);
		p++;
		sleep(1);
	}
	pthread_mutex_unlock(&mutex);
}

int main(int argc,char *argv[])
{

	//初始化互斥锁
	pthread_mutex_init(&mutex,NULL);

	//1,创建2条线程分别输出"hello","world"
	pthread_t tid[2];
	pthread_create(&tid[0],NULL,fun1,NULL);
	pthread_create(&tid[1],NULL,fun2,NULL);

	//2,等待两条子线程结束
	pthread_join(tid[0],NULL);
	pthread_join(tid[1],NULL);

	return 0;
}

在这里插入图片描述
结果正常打印

二、特殊的互斥锁 – 读写锁

问题:在系统中存在成千上万个任务需要对同一个临界资源进行访问,大部分的任务只需要对这个资源进行读取信息(不修改资源内容),如果此时对这个资源进行上锁处理,那么就会导致这个资源只能被某一个任务独占式使用,其他的资源不能访问(陷入阻塞),这样会导致效率降低。

例如:假设博物馆展示一幅画(临界资源),所有去博物馆参观的游客就是系统中的任务,绝大部分是只需要对这个临界资源进行观赏(读取),少部分需要对临界资源进行修改(维修人员),那么这幅画应该可以同时被很多人参观,但是被维修的时候只能被一个人维修。

1、读写锁机制:

读写锁上锁时可以选择读锁或者写锁。

读锁: 读写锁上读锁,其他的任务在对这个读写锁上读锁时不会阻塞,但是上写锁时会阻塞。

写锁:读写锁上写锁时,其他的任务上读锁和写锁都会阻塞。此时写锁相当于互斥锁。

2、相关API:

SYNOPSIS

   #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_rdlock(pthread_rwlock_t *rwlock);	//上读锁
   int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);	//上写锁
   int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);	//解锁

三、线程的同步

1、线程的同步

现在的操作系统大部分都是多用户多任务操作系统,那么在系统中同时存在大量可调度的任务实体(进程)在运行,可能会出现以下情况:

1) 需要同时访问/使用同一个临界变量 (互斥)

2) 多个任务的执行之间存在依赖关系,一个任务运行需要另一个任务运行的结果。(同步)

互斥: 多个任务在使用同一个临界资源时,当一个任务获取到这个资源时,其他所有的任务都不能去访问这个资源,保证这个资源被独占式使用。

同步: 不同的任务按照一个规定的顺序去执行。同步其实是一种高级的互斥。

例如: 进程A : 面粉厂; 进程B:面包厂
在解决互斥问题时,通常使用互斥锁,读写锁。

2、死锁问题

死锁:系统中多个任务在执行过程中因为资源争夺陷入的一种僵局,陷入这种僵局时,如果没有外力的推动下,谁都无法继续往下执行。

例如:进程A执行需要资源a,b; 进程B的执行也需要a,b; 不管是进程A还是进程B只要执行完就会是否a,b资源。
当前系统中的资源只有一个a,一个b, 此时进程A获取资源a, 进程B获取资源b;

死锁产生的四个必要条件:
1)互斥条件 (一个资源在某一时刻只能被一个任务占有)

2)占有等待 (占据一些资源去请求另外的一些资源)

3)非剥夺条件 (已占据的资源不能被其他任务剥夺)

4)环路等待条件

> 资源是有限的!
> 避免产生死锁:破坏任意一个死锁产生的必要条件,死锁就不会产生。
//
=========================================================//
(1)互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

预防死锁
预防死锁的方法是通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或者几个,以避免发生死锁。
由于互斥条件是非共享设备所必须的,不仅不能改变,还应该加以保证,因此,主要是破坏产生死锁的后三个条件。

破坏请求与保持条件: 对一个进程在请求资源时,它不能持有不可剥夺资源。
破坏不可剥夺条件: 对一个已经保持了某些不可剥夺资源的进程,提出新的资源请求而不能得到满足时,它必须释放已经保持的所有资源,待以后需要时再重新申请。
破坏循环等待条件: 对系统所有资源类型进行线性排列,并赋予不同序号。规定每个进程必须按序号递增的顺序请求资源。

避免死锁的方法
一次封锁法:每个进程(事务)将所有要使用的数据全部加锁,否则,就不能继续执行;
顺序封锁法:预先对数据对象规定一个封锁顺序,所有进程(事务)都按这个顺序加锁;
银行家算法:保证进程处于安全进程序列。
//=========================================================//

3、线程间同步功能

信号量 – POSIX信号量
POSIX信号量
无名信号量 : 用于线程间同步
有名信号量 : 用于进程间同步

信号量工作原理:
信号量其实就是资源计数器,用来控制公共资源的访问。

PV原语 : P操作,V操作
P操作: 对信号量的值-1,资源数-1.(如果当前信号量的值为0,p操作会然任务陷入阻塞)
V操作: 对信号量的值+1,资源数+1 (V操作在任何情况下都不会阻塞任务)

例子:
假设你是一家自行车租车店的老板,你有5辆自行车可以拿来出租,顾客可以选择租车,或者是还车,如果当前自行车数量为0,那么顾客来租车时,就会陷入阻塞。

信号量的值: 当前的资源数 5
==> 租车 : P操作 (资源数-1)
==> 还车 : V操作 (资源数+1)
==> 顾客 :使用资源的任务,当一条任务因为申请资源P操作陷入阻塞时,此时必须由另外一个任务进行V操作,使资源量+1,才能解除阻塞。

例子2:

#include <stdio.h>
#include <semaphore.h>
#include <pthread.h>
#include <unistd.h>

sem_t sem;		//信号量变量
int  Bike_Num = 5;

void *time_display(void *arg)
{
	for(int i=0; ; i++)
	{
		printf("time:%d\n", i);
		sleep(1);
	}	
}

/*线程执行函数*/
void *routine(void *arg)
{
	//租车
	if(Bike_Num == 0)
		printf("当前店里没车,请等一下!\n");
	
	sem_wait(&sem);
	Bike_Num--;
	printf("[%lu]租车成功!\n", pthread_self());	//打印线程ID
	
	sleep(10);
	
	//还车
	sem_post(&sem);
	Bike_Num++;
	printf("[%lu]还车车成功!\n", pthread_self());	//打印线程ID
}


/*无名信号量使用*/
int main(int argc, char *argv[])
{
	sem_init(&sem, 0, 5);	//信号量初始值设置为5
	
	//主线程循环获取整数
	int n;
	pthread_t tid, tid1;
	pthread_create(&tid1, NULL, time_display, NULL);
	
	while(1)	//主线程循环获取整数,创建线程
	{
		scanf("%d", &n);
		pthread_create(&tid, NULL, routine, NULL);
	}
	
	
	return 0;
}

在这里插入图片描述

4、相关API

1) sem_init() //信号量初始化

SYNOPSIS

   #include <semaphore.h>

   int sem_init(sem_t *sem, int pshared, unsigned int value);
   Link with -pthread.

==> sem : 信号量变量地址
==> pshared : 0 线程之间的共享
==> value : 信号初始值

返回值:成功返回0,失败返回-1

2)sem_wait() //信号量P操作

SYNOPSIS

   #include <semaphore.h>

   int sem_wait(sem_t *sem);

==> sem : 信号量变量地址
返回值:成功返回0,失败返回-1

3)sem_post() //信号量V操作

SYNOPSIS

   #include <semaphore.h>

   int sem_post(sem_t *sem);

==> sem : 信号量变量地址
返回值:成功返回0,失败返回-1

4)sem_destroy() //无名信号量的销毁

SYNOPSIS

   #include <semaphore.h>

   int sem_destroy(sem_t *sem);

==> sem : 信号量变量地址
返回值:成功返回0,失败返回-1

例子: 使用无名信号量模拟租车还车的案例
主线程循环等待输入数据(scanf()), 每次获取到整数,那就创建一条线程去租车,10s之后还车,如果没有车,那就提示没有车,需要等待,如果有车,那就把车租走。

四、有名信号量

POSIX信号量中,无名信号量用于线程间同步,有名信号量用于进程间同步。

1、相关API:

1)sem_open() //有名信号量初始化

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 : 有名信号量路径名 (有名信号量路径必须在 / )
==> oflag : 有名信号的打开权限 O_CREAT (存在则打开,不存在则创建)
==> mode : 有名信号量的文件权限 0777
==> value : 有名信号量的初始值
返回值: 成功返回有名信号量的地址,失败返回 SEM_FAILED

2) sem_wait(), sem_post() //有名型号量PV操作

3)sem_close() //有名信号量关闭

SYNOPSIS

   #include <semaphore.h>
   int sem_close(sem_t *sem);

4)有名信号量删除 sem_unlink() --> 保证这个信号量必须被sem_close()了

例子:使用有名信号量规范进程A,进程B的同步关系。
进程A每次从键盘输入一串字符串,进程B就打印一句话”get_string”

分析: 进程A, 进程B如何实现同步
信号量1(输入资源), 信号量2(输出资源)

==> 初始状态下,输入资源为1,输出资源为0; (进程A可以运行,进程B不能运行)
==> 进程A输入数据之后,输入资源-1,输出资源+1
==> 进程B输出数据之后,输出资源-1,输入资源+1

假设设置两个信号量 sem_1 = 1; sem_2=0;
进程A 进程B
While(1) while(1)
{ {
P(sem_1); P(sem_2);
//输入数据 //输出数据
V(sem_2); V(sem_1);
} }

练习:参考有名信号量的示例代码,实现以下功能。
使用POSIX有名信号量代替 IPC信号量,实现两个进程对共享内存的同步使用。
进程A循环往共享内存写入数据,进程B输出共享内存的内容。

2、条件变量

在线程中,条件变量的作用是让线程进入睡眠。当某种情况产生的时候,就给线程发送一个唤醒信号,然线程去工作。

条件变量需要跟互斥锁一起使用。

条件变量这个变量本身是一个临界资源,不同的线程在使用的时候需要互斥使用(使用之前上锁)

案例:
小明是一个学生,小明的父母通过一个银行卡给小明转生活费。
小明的父母判断银行卡中的余额,如果余额大于1500,那小明的父母就去上班,不管这个银行卡了,否则小明的父母给银行卡转钱2000。
小明每个月从银行卡中取出1000元作为生活费,如果卡中余额大于1000,那小明就正常的取出金钱,如果卡中余额数量不够1000元,那就打电话通知父母,父母接到信号就去通过银行卡给小明打钱。

==> 条件变量(临界资源) : 银行卡
==> 小明的父母 : 线程1
==> 小明:线程2

例子3:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_cond_t cond;	//条件变量
pthread_mutex_t mutex;	//互斥锁

int Money = 1800;	//银行卡初始值

/*打印时间*/
void *time_play(void *arg)
{
	for(int i = 0; ; i++)
	{
		printf("time:%d\n", i);
		sleep(1);
	}		
}

/*小明的父母*/
void *Parent(void *arg)
{
	while(1)
	{
		pthread_mutex_lock(&mutex);		//上锁
		
		if(Money > 1500)
		{
			printf("[父母]钱还够,我们睡觉了!\n");
			pthread_cond_wait(&cond, &mutex);	//线程睡眠 --> 阻塞
		}
		
		Money += 2000;
		printf("[父母]已经转了2000,余额:%d元\n", Money);
		
		pthread_mutex_unlock(&mutex);	//解锁
	}
	
}

void *Son(void *arg)
{
	while(1)
	{
		pthread_mutex_lock(&mutex);		//上锁
		
		if(Money > 1000)
		{
			Money -= 1000;
			printf("[小明]取出1000生活费,剩余%d元\n", Money);
		}
		else{
			printf("[小明]没钱了,快打钱啊\n");
			pthread_cond_signal(&cond);		//唤醒任意一条睡眠的线程
		}
		pthread_mutex_unlock(&mutex);	//解锁
		
		sleep(3);		//一个月
	}
}


/*条件变量*/
int main(int argc, char *argv[])
{
	//1,初始化条件变量,初始化互斥锁
	pthread_cond_init(&cond, NULL);
	pthread_mutex_init(&mutex, NULL);
	
	//2,创建2条线程
	pthread_t tid[3];
	pthread_create(&tid[0], NULL, time_play, NULL);
	pthread_create(&tid[1], NULL, Parent, NULL);		//小明父母
	pthread_create(&tid[2], NULL, Son, NULL);			//小明
	
	//3,主线程不退出
	pthread_join(tid[0], NULL);
	pthread_join(tid[1], NULL);
	pthread_join(tid[2], NULL);
	
	//4,销毁条件变量
	pthread_cond_destroy(&cond);
	
	return 0;
}

在这里插入图片描述

2,相关API

1)pthread_cond_init() //条件变量初始化

SYNOPSIS

   #include <pthread.h>

   pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

//条件变量初始化
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
//给任意一个被条件变量睡眠的线程发送唤醒信号
int pthread_cond_signal(pthread_cond_t *cond);
//给所有的被条件变量睡眠的线程发送唤醒信号
int pthread_cond_broadcast(pthread_cond_t *cond);
//线程通过条件变量进入睡眠 --> mutex (睡眠的时候会释放锁资源)
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
//销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);

例子4:

进程A

#include <stdio.h>
#include <semaphore.h>
#include <pthread.h>
#include <unistd.h>
#include <fcntl.h>           /* For O_* constants */
#include <sys/stat.h>        /* For mode constants */
#include <signal.h>
#include <stdlib.h>

#define SEM_NAME1 "/sem1"
#define SEM_NAME2 "/sem2"

sem_t *sem_1, *sem_2;

/**********************
函数:sem_quit()
原因:当信号量程序运行后,如果直接CTRL+C关闭程序,下次再运行时
程序无法正常工作,因为上一次代码运行时,信号量进程已经在后台运
行,但没有正常关闭,所以下次运行会出现错误。
通过signal()信号函数来对信号量正常关闭。
SIGINT:为ctrl+c键
 **********************/

void Sem_Quit(int arg)	//退出处理功能函数
{
	//3, 删除有名信号量
	sem_close(sem_1);
	sem_close(sem_2);
	
	sem_unlink(SEM_NAME1);
	sem_unlink(SEM_NAME2);	
	
	exit(0);
}

/*输入数据*/
int main(int argc, char *argv[])
{
	signal(SIGINT, Sem_Quit);
	
	//1, 获取有名信号量 信号量1,信号量2
	sem_1 = sem_open(SEM_NAME1, O_CREAT, 0777, 1);
	sem_2 = sem_open(SEM_NAME2, O_CREAT, 0777, 0);
	if(sem_1 == SEM_FAILED || sem_2 == SEM_FAILED)
	{
		perror("sem_open failed");
		return -1;
	}
	
	//2, 循环输入数据
	char buf[32] = {0};
	while(1)
	{
		sem_wait(sem_1);		//信号量1  P操作
		
		fgets(buf, sizeof(buf), stdin);
		printf("fgets:%s", buf);
		
		sem_post(sem_2);		//信号量1  V操作
	}
	

	
	return 0;
}

进程B

#include <stdio.h>
#include <semaphore.h>
#include <pthread.h>
#include <unistd.h>
#include <fcntl.h>           /* For O_* constants */
#include <sys/stat.h>        /* For mode constants */
#include <signal.h>
#include <stdlib.h>

#define SEM_NAME1 "/sem1"
#define SEM_NAME2 "/sem2"

sem_t *sem_1, *sem_2;

void Sem_Quit(int arg)	//退出处理功能函数
{
	//3, 删除有名信号量
	sem_close(sem_1);
	sem_close(sem_2);
	
	sem_unlink(SEM_NAME1);
	sem_unlink(SEM_NAME2);	
	
	exit(0);
}

/*输入数据*/
int main(int argc, char *argv[])
{
	signal(SIGINT, Sem_Quit);
	
	//1, 获取有名信号量 信号量1,信号量2
	sem_1 = sem_open(SEM_NAME1, O_CREAT, 0777, 1);
	sem_2 = sem_open(SEM_NAME2, O_CREAT, 0777, 0);
	if(sem_1 == SEM_FAILED || sem_2 == SEM_FAILED)
	{
		perror("sem_open failed");
		return -1;
	}
	
	//2, 循环输入数据
	
	while(1)
	{
		sem_wait(sem_2);		//信号量2  P操作
		
		printf("get string!\n");
		
		sem_post(sem_1);		//信号量1  V操作
	}
	
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值