C/C++|关于多线程冲突

本文来自小林coding

哲学家就餐问题

哲学家就餐问题对于互斥访问有限的竞争问题(如IO设备)以类的建模过程十分有用。

方案一:先拿做再拿右

很简单我们直接定一个规矩,每个哲学家都先去拿左边 的叉子、然后去拿右边的叉子;

然后进餐;

最后放下左边的叉子、放下右边的叉子;
但是,如果每个哲学家同时拿到了左边的叉子,就会导致死锁。

方案二:共享区加锁

对于方案一的死锁,我们如何解决?

很简单,在临界区加锁即可。
这样的话,每次进餐只能有一位哲学家能吃。

方案三:根据不同的编号采取不同的动作

我们先重申一下问题,既然方案二使用互斥信号量,会导致只能允许一个哲学家就餐,那么我们就不用它;

方案一的问题在于,会出现所有哲学家同时拿左边刀叉的可能性,那我们就避免哲学家可以同时拿左边的刀叉,用分支结构根据哲学家的编号不同,而采取不同的动作。

让偶数编号的哲学家「先拿左边的拆后拿右边的拆」,奇数编号的哲学家「先拿右边的叉子后拿左边的叉子」

这里写一个简单的伪代码:

cpp 复制代码
#define N 5
semaphore fork[N];

void smart_person(int i) {
	while (TRUE) {
		think();  // 哲学家思考
		if (i % 2 == 0) {
			P(fork[i]); // 拿左边叉子
			P(fork[(i + 1) % N]); // 拿右边叉子
		} else {
			P(fork[(i + 1) % N]); // 先拿右边叉子
			P(fork[i]); // 再拿左边叉子
		}
		
		eat();				 // 哲学家就餐
		
		V(fork[i]);			  // 放左边的叉子
		V(fork[(i + 1) % N]); // 放右边的叉子
	}
}

方案四:比较复杂

我们用一个数组 state 来记录每一位哲学家的三种状态,分别是:进餐状态、思考状态、饥饿状态(试图拿叉子)

我们规定只有两个邻居都没有进餐时,才可以进入进餐状态。

i 个哲学家的左邻右舍,由宏 LEFTRIGHT 定义:

  • LEFT:(i + 5 - 1) % 5
  • RIGHT:(i + 1) % 5
    比如 i 为 2 ,则 LEFT 为1,RIGHT 为3。
cpp 复制代码
#define N 5
#define LEFT (i + N - 1) % N
#define RIGHT (i + 1) % N

#define THINKING 0 // 思考状态
#define HUNGRY 1 // 饥饿状态
#define EATING 2 // 进餐状态

int state[N]; //数组记录每个哲学家的状态

semaphore s[5]; // 每个哲学家一个信号量,初值 0
semaphore mutex; // 互斥信号量,初值为1

void test(int i) {
	if (state[i] == HUNGRY &&
		state[LEFT] != EATING &&
		state[RIGHT] != EATING) {
		state[i] = EATING;//两把叉子到手,进入就餐状态
		V(s[i]);//通知第i个哲学家就餐	
	}
}
// 要么拿到两把叉子,要么被阻塞
void take_forks(int i) {
	P(mutex); //进入临界区
	state[i] = HUNGRY;
	test(i);
	V(mutex);
	P(s[i]); // 没有叉子就阻塞,有就继续执行
}
//两把叉子放回原处,并在需要的时候唤醒左邻右舍
void put_forks(int i) {
	P(mutex);
	state[i] = THINKING; // 吃完饭叫出叉子标记思考状态
	test(LEFT);
	test(RIGHT);
	V(mutex);
}

void smart_person(int i) {
	while(TRUE) {
		think();
		take_forks(i);//准备去拿叉子
		eat();
		put_forks(i); // 吃完饭放叉子
	}
}

读者-写者问题

它为数据库访问建立了一个模型。

读者只会读取数据,不会修改数据,而写者即可以读数据也可以修改数据。

  • 【读-读】允许:同一时刻,允许多个读者同时读
  • 【读-写】互斥:没有写者时读者才能读,没有读者时写者才能写
  • 【写-写】互斥:没有其他写着,写着才能写。

方案一:用信号量

信号量wMutex:控制写操作的互斥信号量,初始值为1;

读者计数rCount:正在进行读操作的读者个数,初始化为0;

信号量rCountMutex:控制对rCount读者计数器的互斥修改,初始值为1;

cpp 复制代码
semaphore wMutex; // 控制写操作的互斥信号量,初始值为1
semaphore rCountMutex; // 控制对Rcount 的互斥修改,初始值为1
int rCount = 0; // 正在进行读操作的读者个数,初始化为0

//写进程/线程执行函数
void writer() {
	while (TRUE) {
		P(wMutex); // 进入临界区
		write();
		V(wMutex); //进入临界区
	}
}

void reader() {
	while (TRUE) {
		P(rCountMutex); //进入临界区
		if (rCount == 0) {
			P(wMutex); //如果有写者,阻塞写者
		}
		rCount++; // 读者计数+1
		V(rCountMutex); //离开临界区
		read(); // 读数据
		P(rCountMutex); // 进入临界区
		rCount--; // 读完数据,准备离开
		if (rCount == 0) {
			V(wMutex); //最后一个读者离开,唤醒写者
		}
		V(rCountMutex); // 离开临界区
	}
}

上面这个实现是读者优先,只要读者正在读,后来的读者都可以直接进入,如果读者不断进入,写着会处于饥饿状态

方案二:写者优先

  • 只要有写者准备要写入,写者应尽快执行写操作,后来的读者必须阻塞;
  • 如果有写者持续不断写入,则读者处于饥饿。

方案一的基础上增加如下变量:

这里的 rMutex 的作用:开始有多个读者读数据,他们全部进入读者队列,此时来了一个写者,执行了 P(rMutex) 之后,后续的读者由于阻塞在了rMutex 上,都不能再进入读者队列,而写者到来,则可以全部进入写者队列,因此保证了写者优先。

同时,第一个写者执行了 P(rMutex) 之后,也不能马上开始写,必须等到所有进入读者队列的读者都执行完读操作,通过 V(wDataMutex) 唤醒写者的写操作。

⭐️方案三:公平策略

  • 优先级相同;
  • 写者、读者互斥访问;
  • 只能一个写者访问临界区;
  • 可以有多个读者同时访问临界区;
相关推荐
励志成为嵌入式工程师28 分钟前
c语言简单编程练习9
c语言·开发语言·算法·vim
捕鲸叉1 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer1 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq1 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml41 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~1 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616882 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
aloha_7892 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
记录成长java2 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
hikktn3 小时前
如何在 Rust 中实现内存安全:与 C/C++ 的对比分析
c语言·安全·rust