信号量机制和生产者消费者问题

信号量机制和生产者消费者问题

文章目录

一、前言

先前理解了竞态条件下的软件思路的解决方法------忙等,当然存在一定问题:没有遵循让权等待 。当遇到长时间事件时,这种方法特别消耗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调用

核心:依然是解决资源数量问题

四、小结

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

相关推荐
雯宝5 小时前
mnt_init 函数
linux
律宏阔11 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔11 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴11 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千12 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei3712 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei3712 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei3712 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
GeW12 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux
GeW12 小时前
工业场景中的Linux与数据库选型:高端化转型的技术底座
linux