信号量机制和生产者消费者问题
文章目录
- 信号量机制和生产者消费者问题
-
- 一、前言
- 二、信号量机制
-
- [2.1 优先级反转问题](#2.1 优先级反转问题)
-
- [2.1.1 概述](#2.1.1 概述)
- [2.1.2 原因剖析](#2.1.2 原因剖析)
- [2.1.3 解决方法](#2.1.3 解决方法)
- [2.2 睡眠与唤醒](#2.2 睡眠与唤醒)
-
- [2.2.1 概述](#2.2.1 概述)
- [2.2.2 具体做法](#2.2.2 具体做法)
- 三、生产者和消费者问题
-
- [3.1 场景](#3.1 场景)
- [3.2 代码模拟](#3.2 代码模拟)
- [3.3 信号量](#3.3 信号量)
-
- [3.3.1 概述](#3.3.1 概述)
- [3.3.2 细节](#3.3.2 细节)
- [3.4 互斥量](#3.4 互斥量)
-
- [3.4.1 概述](#3.4.1 概述)
- 四、Futex
- 五、线程库里面的互斥锁
-
- [5.1 接口](#5.1 接口)
- [5.2 代码](#5.2 代码)
- [5.3 和条件变量有关的pthread调用](#5.3 和条件变量有关的pthread调用)
- 四、小结
一、前言
先前理解了竞态条件下的软件思路的解决方法------忙等,当然存在一定问题:没有遵循让权等待 。当遇到长时间事件时,这种方法特别消耗CPU资源,因此有另一种方法专门解决这种问题------睡眠与唤醒
二、信号量机制
2.1 优先级反转问题
2.1.1 概述
有两个进程,基于优先级的调度算法:优先级高的先抢占CPU
概述:当B在执行时,优先级更高的A来了,此时CPU会放弃B,先执行A,执行完A才进行B。
饿死:当一直执行高优先级队列中的任务,使得低优先级的任务无法执行,就会发生饿死
FreeRTOS实时操作系统支持的调度算法就是先读高再读低(只要有高优先级,低优先级就一定不会执行)
非饿死:对于高优先级队列设置一定的调度限制,到达限制,就执行低优先级的任务
Linux就是采用非饿死的调度机制
A进程进入scanf函数之后,开始读取键盘,此时用户没有敲,A会放到磁盘驱动的等待队列里(scanf会读取键盘的驱动),高优先级就没有任务了,调度算法只能从低优先级里面取了,取到B。B开始执行,如果执行了加锁操作(把门锁了,在里面干活------执行临界区代码),干活过程中B时间片到了,CPU就把B调到就绪队列中,B再响应其他进程(加锁干活),刚好键盘来了,A从驱动被唤醒到优先级队列里。A也想加锁临界资源(从B那里拿锁------B先干完活,再解锁),但是A优先级高,就无法使得B继续运行。最终导致:B的锁永远释放不了,A也加不了锁。
2.1.2 原因剖析
加锁是轮询方式。加锁一直在耗CPU,A不可能放弃CPU,B没有机会再执行(临界区代码)

这就会出现优先级反转。(按正常说,B应该先运行,但是运行不了了)
2.1.3 解决方法
A加不了锁就不要老是抢CPU了,而是应该置于睡眠状态(加睡眠锁,即互斥锁),放弃CPU
加锁:
- 轮询方式(自旋锁)
- 睡眠方式(互斥锁)
睡眠锁也有一个等待队列(lock)。当B执行完,就解锁(设置一个状态)。A也解锁------唤醒,就是唤醒锁的队列,A就又被放到高优先级队列中了。
2.2 睡眠与唤醒
目的:
- 优先级反转问题
- 防止轮询耗费CPU
2.2.1 概述
- 睡觉:放弃CPU
- 唤醒:防止睡眠状态的调度体永远得不到CPU响应
2.2.2 具体做法
不能纯软件来做,要靠硬件的原语操作(因为睡眠与唤醒一定是不能被打断的):
- 睡眠原语
- 唤醒原语
目的:保证程序执行的正确性
整型信号量:轮询(不太用)
记录型信号量 :睡眠和唤醒队列,即等待队列。sleep(调度体),当前这个调度体就没有机会了,只能靠别人调用wake-up原语唤醒对应的等待队列
三、生产者和消费者问题
3.1 场景
两个进程争苹果。有一个进程专门生产苹果(生产者),消费者(另一个进程)不断吃苹果,此时就会出现一个问题,装苹果的仓库大小有限------一次只能放10个苹果,这就导致生产苹果是有限制的。有两个矛盾:生产者什么时候生产,消费者什么时候能吃。这就称为"有界缓存区问题",即生产者消费者问题。
3.2 代码模拟
伪代码,看看就好,主要是理解
生产者消费者本身的行为
c
#include <stdio.h>
#define FALSE 0
#define TRUE 1
// 缓存区slot(槽)的数量,即存储苹果的地方
#define N 100
// 缓存区数据的数量,使用volatile关键字防止编译器优化掉不必要的读取
volatile int count = 0;
extern void consumer(void);
// 模拟生成数据项的函数(示例),即生产者
int produce_item(void)
{
// 这里应该实现生产数据项的逻辑,这里简单返回静态值作为示例
static int item = 0;
return ++item;
}
// 模拟消费数据项的函数(示例),即消费者
void consumer_item(int item)
{
// 这里应该实现消费者数据项的逻辑,这里仅打印作为示例
printf("Consumed: %d\n", item);
}
// 假设的插入数据项到缓存区的函数(需具体实现),即生产完苹果放入仓库
void insert_item(int item)
{
// 这里应实现将数据项放入缓存区的逻辑
// 注意:实现实际中可能需要考虑缓存区同步和互斥访问
}
// 假设的从缓存区移除数据项的函数(需具体实现),即消费者从仓库取苹果
int remove_item(void)
{
// 这里应实现从缓存区取出数据项的逻辑
// 注意:实际实现中同样需要考虑同步和互斥
return 0; // 示例返回
}
// 使用两个原语
// 模拟系统调用sleep,这里使用忙等待(实际应使用系统提供的阻塞机制)
void sleep(void) // 两种:轮询、睡眠
{
// 这里仅为示例,实际应使用系统提供的sleep函数
// 在这里,我们简单让出CPU时间片,但不是真正的sleep
// 实际应用中,应使用如POSIX的sleep()或Windows的Sleep()
}
// 模拟系统调用wakeup,这里假设有某种机制可以唤醒其他线程或进程
void wakeup(void (*func)(void))
{
// 这里仅为示例,实际应使用如POSIX的pthread_cond_signal或Windows的SetEvent
// 这里我们不做任何操作,因为真正的唤醒机制依赖于操作系统
}
// 生产者
void producer(void)
{
int item;
// 源源不断生产,有人来消费
while (TRUE)
{
item = produce_item();
// 如果缓存区是满了,就阻塞(忙等)
while(count == N) // 放入缓存区判满
{
sleep();
}
insert_item(item);
count++;
if(count == 1)
{
wakeup(consumer); // 一个状态:表示可以消费东西了
}
}
}
// 消费者
void consumer(void)
{
int item;
// 不断吃东西
while(TRUE)
{
// 如果缓存区是空的,就阻塞(忙等)
while(count == 0)
{
sleep(); // 睡觉需要有人唤醒
}
item = remove_item(); // 从缓存移出东西
consumer_item(item); // 和上面一起
count--;
if(count == N - 1) // 只唤醒一次
{
wakeup(producer); // 这是告诉一个状态:可以生产
}
}
}
问题:
- 睡眠丢失:程序执行过程的问题。如果生产者先唤醒又去生产,缓存区的影响,=0,消费者sleep了,就再也不会唤醒了。
sleep和wakeup都是轮询,消耗CPU,状态得不到(没有人唤醒就一直睡眠)
解决:
提出了信号量,信号量可以解决同步和互斥问题(基于底层机制)。
尽管信号量可以解决两类问题,但是之后提到信号量 就是解决同步 问题,和互斥 相关的是互斥量
3.3 信号量
3.3.1 概述
Dijkstra提出的。
先前在最短路径中也有提及
信号量的核心就是睡眠和唤醒。
信号量到底怎么定义的呢?
教材版本(推荐):
信号量含义:衡量资源数量的一个数据类型,它表示的值定义当前系统的一个状态,在临界资源访问时,有多少进程等待该资源,有多少资源可供其他人使用
c
struct semaphore // 记录型信号量(结构体)
{
int value; // 资源的数量
queue<struct task_struct*> que; // 这个资源的等待队列,队列里放的元素就是进程标识
};
睡眠:down(pid)、sleep、P操作、wait(系统调用)
c
void P(semaphore S)
{
pid = getpid();
S->value--; // 吃东西
if(S->value < 0) // 因为是先-,因此就会是<0
{
wait(S->que, pid);
}
}
唤醒:up()、wake_up、V操作
c
void V(semaphore S)
{
S->value++; // 送东西
if(S->value <= 0) // <= 0才是有人睡觉
{
wake_up(S->que);
}
}
扩展版本:
down不是先--,而是先判断;up也是先判断,有人睡觉就唤醒,直到已经没有人了,才是++
3.3.2 细节
信号量值为1-互斥锁
信号量值为n-临界值资源多个
代码
伪代码,理解为主~
c
#include <stdio.h>
#include <sys/types.h>
#define FALSE 0
#define TRUE 1
// 缓存区slot(槽)的数量
#define N 100
struct queue{ pid_t pid; };
// typedef struct
// {
// int value;
// struct queue wq;
// } semaphore;
typedef int semaphore;
extern void down(semaphore *);
extern void up(semaphore *);
int produce_item(void)
{
static int item = 0;
return ++item;
}
void consumer_item(int item)
{
printf("Consumed: %d\n", item);
}
void insert_item(int item)
{
}
int remove_item(void)
{
return 0;
}
// 标明缓存区有空位置的个数-空位置实现
semaphore empty = N; // 缓存区初始化时有N个空位
// 标明缓存区已经放入资源的数量
semaphore full = 0; // 缓存区初始化时有0个可用资源
// 控制区临界区互斥访问(有同时写入的行为-互斥)
semaphore mutex = 1; // 表示互斥锁(0/1两种状态,初始化永远为1)
// 生产者
void producer(void)
{
int item;
while(TRUE) // 大框架: 生产->插入
{
item = produce_item(); // 生产
// 等待缓存区有空位置
down(&empty); // 谁down谁等,有down就得有up
down(&mutex); // 操作锁
insert_item(item); // 插入
up(&mutex);
up(&full); // 通知放了一个
}
}
// 消费者
void consumer(void)
{
int item;
while(TRUE) // 大框架: 先取->吃掉
{
// 等待缓存区有东西,才能移出
down(&full);
down(&mutex);
item = remove_item(); // 取
up(&mutex);
up(&empty); // 唤醒(释放)
consumer_item(item); // 处理
}
}
互斥锁是保护临界区的(互斥就是我用的时候你不能用)
核心代码思路:
- 没加锁的原本思路理清(大框架)
- 先后问题
- PV配对写,什么时候写?(有P就有V)
- 考虑加互斥(出现同时写入的情况)
3.4 互斥量
3.4.1 概述
互斥量就是互斥锁,整个资源数量就是1或0,即:unlocked-解锁,locked-加锁,对应接口解锁-mutex_unlock(),加锁-mutex_lock()
锁的内部也是信号量实现的(包含睡眠唤醒)
四、Futex
自旋锁(短、快)和互斥锁(队列,慢)结合体,实现分为:内核服务、用户库
先自旋锁试探,可以快速转换互斥锁
五、线程库里面的互斥锁
5.1 接口
| 线程调用 | 描述 |
|---|---|
| Pthread_mutex_init | 创建一个互斥量 |
| Pthread_mutex_destroy | 撤销一个已存在的互斥量 |
| Pthread_mutex_lock | 获得一个锁或堵塞(唤醒) |
| Pthread_mutex_trylock | 获得一个锁或失败(尝试) |
| Pthread_mutex_unlock | 释放一个锁(睡眠) |
5.2 代码
先前在没加锁的时候,运行就会出现死循环(因编译执行过程中时间片的打断),当执行加上锁会怎样呢?
代码1:
c
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<stdlib.h>
#include<bits/pthreadtypes.h>
int value1 = 0;
int value2 = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 全局变量-共享
// task1和task2都必须是同一把锁(竞争)
void task1(int flags)
{
int cnt = 0;
while(1)
{
cnt++;
pthread_mutex_lock(&mutex);
value1 = cnt; // 要么同时赋值cnt,要么都不加(互斥)
value2 = cnt;
pthread_mutex_unlock(&mutex);
}
}
void task2(int flags)
{
while(1)
{
pthread_mutex_lock(&mutex);
if(value1 != value2)
{
printf("value1 = %d, value2 = %d\n", value1, value2);
}
pthread_mutex_unlock(&mutex);
}
}
int main()
{
pthread_t tid1, tid2;
// pthread_mutex_init(&mutex, NULL);
int ret = pthread_create(&tid1, NULL, (void *(*)(void *))task1, (void *)100);
if(ret)
{
perror("pthread create");
exit(-1);
}
// 创建第二个线程
ret = pthread_create(&tid2, NULL, (void *(*)(void *))task2, (void *)200);
if(ret)
{
perror("pthread create");
exit(-1);
}
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("main process!\n");
return 0;
}
运行结果:

先解决互斥,再看先后
代码2:
c
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<stdlib.h>
#include<bits/pthreadtypes.h>
int value1 = 0; // 一个临界资源
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void task1(int flags)
{
for (int i = 0; i < 1000; i++) // 干活的时候不被打扰
{
pthread_mutex_lock(&mutex);
value1++;
pthread_mutex_unlock(&mutex);
}
}
void task2(int flags)
{
for (int i = 0; i < 1000; i++)
{
pthread_mutex_lock(&mutex);
value1++;
pthread_mutex_unlock(&mutex);
}
}
int main()
{
pthread_t tid1, tid2;
int ret = pthread_create(&tid1, NULL, (void *(*)(void *))task1, (void *)100);
if(ret)
{
perror("pthread create");
exit(-1);
}
// 创建第二个线程
ret = pthread_create(&tid2, NULL, (void *(*)(void *))task2, (void *)200);
if(ret)
{
perror("pthread create");
exit(-1);
}
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("main process value1 = %d!\n", value1);
return 0;
}
运行结果:

线程的资源共享,会有很多临界区,就需要考虑同步和互斥
5.3 和条件变量有关的pthread调用

核心:依然是解决资源数量问题
四、小结
本篇介绍了硬件视角下的解决产时间事件的竞态条件的方案------睡眠与唤醒,其底层就是信号量机制。在同步互斥问题中(如:生产者消费者问题),信号量是解决这种问题的关键
