《OSTEP》信号量(chap31)

〇、前言

本文是对《OSTEP》第三十一章的实践与总结。

一、信号量

以下各个版本都是一个生产者-消费者模型,基于信号量实现,并且逐渐完善。

#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#define MAX 10

int buffer[MAX];
int fill = 0;
int use = 0;

// 生产行为
void put(int value) {
    buffer[fill] = value;
    fill = (fill + 1) % MAX;
}

// 消费行为
int get() {
    int temp = buffer[use];
    use = (use + 1) % MAX;
    return temp;
}
sem_t *empty;
sem_t *full;

// 生产者
void *producer(void *arg) {
    int loops = *((int *)arg);
    for (int i = 0; i < loops; i++) {
        sem_wait(empty);
        put(i);
        sem_post(full);
    }
    return NULL;
}
// 消费者
void *consumer(void *arg) {
    int value = 0;
    int loops = *((int *)arg);
    for (int i = 0; i < loops; i++) {
        sem_wait(full);
        value = get();
        sem_post(empty);
        printf("消费:%d\n", value);
    }
    return NULL;
}
int main() {
    // sem_init(&empty, 0, MAX);
    // sem_init(&full, 0, 0);
    // 以上写法已经在 macOS 中废弃
    empty = sem_open("empty", O_CREAT, S_IRUSR | S_IWUSR, MAX);
    full = sem_open("full", O_CREAT, S_IRUSR | S_IWUSR, 0);
    pthread_t p1, p2;
    int loop = 10;
    pthread_create(&p1, NULL, producer, &loop);
    pthread_create(&p2, NULL, consumer, &loop);

    pthread_join(p1, NULL);
    pthread_join(p2, NULL);

    sem_close(empty);
    sem_close(full);

    sem_unlink("empty");
    sem_unlink("full");

    return 0;
}

运行结果:

****** chap31_信号量 % ./a
消费:0
消费:1
消费:2
消费:3
消费:4
消费:5
消费:6
消费:7
消费:8
消费:9

可以看到在单线程下(一个生产者线程,一个消费者线程)运行得很好。

int main() {
...
    pthread_t p1, p2, p3;
    int loop1 = 10;
    int loop3 = 10;
    pthread_create(&p1, NULL, producer, &loop1);
    pthread_create(&p2, NULL, producer, &loop1);
    pthread_create(&p3, NULL, consumer, &loop3);
...
}

运行结果:

****** chap31_信号量 % ./a
消费:0
消费:1
消费:2
消费:3
消费:4
消费:5
消费:0
消费:6
消费:1
消费:7

运行结果可能看不出什么,但是生产者产生的值会被覆盖。这个情况是怎么发生的呢?

假设两个生产者(Pa和Pb)几乎同时调用put()。当Pa先运行,在f1行先加入第一条数据(fill=0),假设Pa在将fill计数器更新为1之前被中断,Pb开始运行,也在f1行给缓冲区的0位置加入一条数据,这意味着那里的老数据被覆盖。

因此,我们的解决思路是将 put()修饰成一个临界区。

sem_t *empty;
sem_t *full;
sem_t *mutex;

// 生产者
void *producer(void *arg) {
    int loops = *((int *)arg);
    for (int i = 0; i < loops; i++) {
        sem_wait(mutex);
        sem_wait(empty);
        put(i);
        sem_post(full);
        sem_post(mutex);
    }
    return NULL;
}
// 消费者
void *consumer(void *arg) {
    int value = 0;
    int loops = *((int *)arg);
    for (int i = 0; i < loops; i++) {
        sem_wait(full);
        value = get();
        sem_post(empty);
        printf("消费:%d\n", value);
    }
    return NULL;
}
int main(){
	...
	mutex = sem_open("mutex", O_CREAT, S_IRUSR | S_IWUSR, 1);
	...
}

运行结果:

****** chap31_信号量 % ./a
消费:0
消费:1
消费:2
消费:0
消费:3
消费:1
消费:4
消费:2
消费:5
消费:3

那如果继续增加一个消费者呢?

#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#define MAX 10

int buffer[MAX];
int fill = 0;
int use = 0;

// 生产行为
void put(int value) {
    buffer[fill] = value;
    fill = (fill + 1) % MAX;
}

// 消费行为
int get() {
    int temp = buffer[use];
    use = (use + 1) % MAX;
    return temp;
}
sem_t *empty;
sem_t *full;
sem_t *mutex;

// 生产者
void *producer(void *arg) {
    int loops = *((int *)arg);
    for (int i = 0; i < loops; i++) {

        sem_wait(empty);
        sem_wait(mutex);
        put(i);
        sem_post(mutex);
        sem_post(full);
    }
    return NULL;
}
// 消费者
void *consumer(void *arg) {
    int value = 0;
    int loops = *((int *)arg);
    for (int i = 0; i < loops; i++) {

        sem_wait(full);
        value = get();
        sem_post(empty);

        printf("消费:%d\n", value);
    }
    return NULL;
}
int main() {
    empty = sem_open("empty", O_CREAT, S_IRUSR | S_IWUSR, MAX);
    full = sem_open("full", O_CREAT, S_IRUSR | S_IWUSR, 0);
    mutex = sem_open("mutex", O_CREAT, S_IRUSR | S_IWUSR, 1);
    pthread_t p1, p2, p3, p4;
    int loop1 = 3;
    int loop2 = 3;
    pthread_create(&p1, NULL, producer, &loop1);
    pthread_create(&p2, NULL, producer, &loop1);
    pthread_create(&p3, NULL, consumer, &loop2);
    pthread_create(&p4, NULL, consumer, &loop2);

    pthread_join(p1, NULL);
    pthread_join(p2, NULL);
    pthread_join(p3, NULL);
    pthread_join(p4, NULL);

    sem_close(empty);
    sem_close(full);
    sem_close(mutex);

    sem_unlink("empty");
    sem_unlink("full");
    sem_unlink("mutex");

    return 0;
}

运行结果:

****** chap31_信号量 % ./a
消费:0
消费:0
消费:1
消费:2
消费:1
消费:3
消费:2
消费:3
消费:5
消费:4
消费:6
消费:5
消费:7
消费:6
消费:4
消费:7
消费:8
消费:9
消费:8
消费:9

运行得非常好,有一个问题,会不会出现重复消费行为(把一个值消费两次)?看起来好像会。
但是记得 full 信号量,初始值为 0,这意味着每次只允许一个线程进行消费。这不仅和生产者达成了正确的执行序列,而且还间接得使得每次只能有一个线程在 get()

二、读写锁

以下是利用信号量来实现一个读写锁。
数据在有线程读时,不能写。这意味着第一个线程读的时候,直接将写者锁占有,最后一个读者将写锁释放。以下是代码:

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

// 读取的空间
char space[10] = "hello";

// 读者写者锁
typedef struct _rwlock_t {
    sem_t *lock;
    sem_t *writelock;
    int readers;
} rwlock_t;

// 初始化
void rwlock_init(rwlock_t *rw) {
    rw->readers = 0;
    rw->lock = sem_open("rw_lock", O_CREAT, S_IRUSR | S_IWUSR, 1);
    rw->writelock = sem_open("rw_writelock", O_CREAT, S_IRUSR | S_IWUSR, 1);
}
// 获得读锁(实际上就不需要上锁,上锁只是为了修改 rw)
void rwlock_acquire_readlock(rwlock_t *rw) {
    sem_wait(rw->lock);
    rw->readers++;
    if (rw->readers == 1) {
        sem_wait(rw->writelock); // 第一个读者需要抢占写锁
    }
    sem_post(rw->lock);
}
// 释放读锁(实际上就不需要上锁,上锁只是为了修改 rw)
void rwlock_release_readlock(rwlock_t *rw) {
    sem_wait(rw->lock);
    rw->readers--;
    if (rw->readers == 0) {
        sem_post(rw->writelock); // 最后一个读者需要释放写锁
    }
    sem_post(rw->lock);
}
// 获得写锁
void rwlock_acquire_writelock(rwlock_t *rw) { sem_wait(rw->writelock); }
// 释放写锁
void rwlock_release_writelock(rwlock_t *rw) { sem_post(rw->writelock); }

rwlock_t rwlock;

// 写线程
void *write_t(void *arg) {
    rwlock_acquire_writelock(&rwlock);
    strcpy(space, arg);
    rwlock_release_writelock(&rwlock);
    return NULL;
}

// 读线程
void *read_t(void *arg) {
    rwlock_acquire_readlock(&rwlock);
    printf("%s\n", space);
    rwlock_release_readlock(&rwlock);
    return NULL;
}

int main() {
    rwlock_init(&rwlock);
    char arg[10] = "1111111";
    // 创建 5 个读线程和 5 个写线程
    pthread_t p_r1, p_r2, p_r3, p_r4, p_r5;
    pthread_t p_w1, p_w2, p_w3, p_w4, p_w5;

    pthread_create(&p_r1, NULL, read_t, NULL);
    pthread_create(&p_r2, NULL, read_t, NULL);

    pthread_create(&p_w1, NULL, write_t, arg);
    arg[2] = '2';
    pthread_create(&p_w2, NULL, write_t, arg);

    arg[5] = '5';
    pthread_create(&p_w5, NULL, write_t, arg);

    pthread_create(&p_r3, NULL, read_t, NULL);
    pthread_create(&p_r4, NULL, read_t, NULL);
    arg[3] = '3';
    pthread_create(&p_w3, NULL, write_t, arg);
    arg[4] = '4';
    pthread_create(&p_w4, NULL, write_t, arg);
    pthread_create(&p_r5, NULL, read_t, NULL);

    pthread_join(p_r1, NULL);
    pthread_join(p_r2, NULL);
    pthread_join(p_r3, NULL);
    pthread_join(p_r4, NULL);
    pthread_join(p_r5, NULL);
    pthread_join(p_w1, NULL);
    pthread_join(p_w2, NULL);
    pthread_join(p_w3, NULL);
    pthread_join(p_w4, NULL);
    pthread_join(p_w5, NULL);
    sem_close(rwlock.lock);
    sem_close(rwlock.writelock);

    sem_unlink("rw_lock");
    sem_unlink("rw_writelock");

    return 0;
}

运行结果:

****** chap31_信号量 % ./a
hello
hello
1121151
1121151
1123451

可以顺利运行。

三、哲学家就餐问题

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
// 模拟四位哲学家就餐,使得他们能顺利就餐,并且不会有人挨饿.
// 拿到餐具的人,吃完饭后就立即放下餐具,让给其他人(看起来不是那么卫生)

// 设置餐具

sem_t forks[5];
// 初始化这些叉子
void forks_init(){
    for(int i = 0; i < 5; i++){
        sem_init(&forks[i],0,1);
    }
}

// 假设哲学家编号为 p,则他拿的叉子(左右两个叉子)编号应该为:
int left(int p){
    return p;
}
int right(int p){
    return (p+1)%5;
}

// 拿到叉子,准备吃饭
void getforks(int p){
    if(p == 4){
        sem_wait(&forks[right(p)]);
        sem_wait(&forks[left(p)]);
        // 拿到了两个叉子就能吃饭了
        printf("%d号哲学家开始就餐.\n",p);
        sleep(2);
    }else{
    sem_wait(&forks[left(p)]);
    sem_wait(&forks[right(p)]);
    // 拿到了两个叉子就能吃饭了
    printf("%d号哲学家开始就餐.\n",p);
    sleep(2);
    }
}
// 放下餐具
void putforks(int p){
    printf("%d号哲学家完成就餐,并放下了叉子.\n",p);
    sem_post(&forks[left(p)]);
    sem_post(&forks[right(p)]);
}

// 就餐
void *eat(void *arg){
    int p = *((int*)arg);
    getforks(p);
    putforks(p);
    return NULL;
}


int main(){
    forks_init();
    pthread_t p[5];
    int i0 = 0;
    pthread_create(&p[0],NULL,eat,&i0);
    int i1 = 1;
    pthread_create(&p[1],NULL,eat,&i1);
    int i2 = 2;
    pthread_create(&p[2],NULL,eat,&i2);
    int i3 = 3;
    pthread_create(&p[3],NULL,eat,&i3);
    int i4 = 4;
    pthread_create(&p[4],NULL,eat,&i4);

    for(int i = 0; i < 5; i++){
        pthread_join(p[i],NULL);
        sem_close(&forks[i]);
    }
    return 0;
}

运行结果(这里是在 Linux 平台):

******:~/OSTEP_notes_linux_version/chap31_信号量# gcc -o a phi_eat.c -lrt -lpthread
******:~/OSTEP_notes_linux_version/chap31_信号量# ./a
0号哲学家开始就餐.
2号哲学家开始就餐.
0号哲学家完成就餐,并放下了叉子.
2号哲学家完成就餐,并放下了叉子.
4号哲学家开始就餐.
1号哲学家开始就餐.
4号哲学家完成就餐,并放下了叉子.
1号哲学家完成就餐,并放下了叉子.
3号哲学家开始就餐.
3号哲学家完成就餐,并放下了叉子.

可以看到所有的哲学家都能顺利完成就餐。这里解决的核心思路就是,最后一个哲学家拿刀叉的顺序和别的哲学家不一样,这在理论上不会造成死锁。
试分析一下(p 代表哲学家,f 代表叉子),易知假设在 t 时刻是一个环形等待:

  • p0 占据 f0 请求 f1;
  • p1 占据 f1 请求 f2;
  • p2 占据 f2 请求 f3;
  • p3 占据 f3 请求 f0;

画出资源配置图之后,很容易求出它的可达性矩阵:
在这里插入图片描述

因此,可达性矩阵为:
P = ( 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 ) P = \left( \begin{matrix} 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 \\ \end{matrix} \right) P= 1111111111111111
强分图也就是:
P ⋀ P T = ( 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 ) P\bigwedge P^T = \left( \begin{matrix} 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 \\ \end{matrix} \right) PPT= 1111111111111111

因此,在 t 时刻处于死锁状态,这与我们的经验相一致。

四、实现一个信号量

这里利用锁和条件变量来实现一个信号量。

#include <pthread.h>

// 自己定义信号量
typedef struct {
    int value;
    pthread_cond_t cond;
    pthread_mutex_t lock;
}zem_t;

// 初始化
void zem_init(zem_t *zem,int value){
    zem->value = value;
    pthread_cond_init(&zem->cond,NULL);
    pthread_mutex_init(&zem->lock,NULL);
}

// wait
void zem_wait(zem_t *zem){
    pthread_mutex_lock(&zem->lock);
    while(zem->value <= 0){
        pthread_cond_wait(&zem->cond,&zem->lock);
    }
    zem->value--;
    pthread_mutex_unlock(&zem->lock);
}

// post
void *zem_post(zem_t *zem){
    pthread_mutex_lock(&zem->lock);
    zem->value++;
    pthread_cond_signal(&zem->cond);
    pthread_mutex_unlock(&zem->lock);
}

这确实是相当优雅。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/139823.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

『亚马逊云科技产品测评』活动征文|搭建带有“弱”图像处理功能的流媒体服务器

授权声明&#xff1a;本篇文章授权活动官方亚马逊云科技文章转发、改写权&#xff0c;包括不限于在 Developer Centre, 知乎&#xff0c;自媒体平台&#xff0c;第三方开发者媒体等亚马逊云科技官方渠道。 本文基于以下软硬件工具&#xff1a; aws ec2 frp-0.52.3 mediamtx-1.3…

RT-DETR算法优化改进: 一种新颖的可扩张残差(DWR)注意力模块,加强不同尺度特征提取能力

💡💡💡本文全网首发独家改进:一种新颖的可扩张残差(DWR)注意力模块,加强不同尺度特征提取能力,创新十足,独家首发适合科研 1)代替RepC3进行使用; 2)DWR直接作为注意力进行使用; 推荐指数:五星 RT-DETR魔术师专栏介绍: https://blog.csdn.net/m0_63774211/…

双十一钜惠!三门不可多得的HarmonyOS学习教程

今年双十一&#xff0c;各大商城优惠不断。这里介绍三门不可多得的HarmonyOS学习教程&#xff0c;都有非常大的折扣优惠。 《鸿蒙HarmonyOS手机应用开发实战》 《鸿蒙HarmonyOS手机应用开发实战》是由清华大学出版社出版的。 目前当当是“7.56折”&#xff1a;http://produc…

RT-DETR算法优化改进:新颖的多尺度卷积注意力(MSCA),即插即用,助力小目标检测 | NeurIPS2022

💡💡💡本文独家改进: 多尺度卷积注意力(MSCA),有效地提取上下文信息,新颖度高,创新十足。 1)代替RepC3进行使用; 2)MSCAAttention直接作为注意力进行使用; 推荐指数:五星 RT-DETR魔术师专栏介绍: https://blog.csdn.net/m0_63774211/category_12497375.ht…

app软件开发多少钱?功能会影响价格吗?

随着智能手机的普及&#xff0c;app开发市场日益繁荣&#xff0c;很多人都有开发app的梦想&#xff0c;但开发一款app需要多少钱呢?功能是否会影响价格?本文将为你揭开这个谜团。 一、app开发费用的影响因素 app开发费用受到多种因素的影响&#xff0c;例如开发难度、功能复…

【Linux】第十六站:进程地址空间

文章目录 一、程序地址空间1.内存的分布2.static修饰后为什么不会被释放3.一个奇怪的现象 二、进程地址空间1.前面现象的原因2.地址空间究竟是什么&#xff1f;3.为什么要有进程地址空间4.页表5.什么叫进程&#xff1f;6.进程具有独立性。为什么&#xff1f;怎么做到呢&#xf…

通讯协议学习之路(实践部分):SPI开发实践

通讯协议之路主要分为两部分&#xff0c;第一部分从理论上面讲解各类协议的通讯原理以及通讯格式&#xff0c;第二部分从具体运用上讲解各类通讯协议的具体应用方法。 后续文章会同时发表在个人博客(jason1016.club)、CSDN&#xff1b;视频会发布在bilibili(UID:399951374) 本文…

2023/11/13JAVA学习

字节数组增大的同时,运行速度也会加快,但是大到一定程度就不行了 要想追加数据,要在低级流后面加true,高级流后面加不了 不是乱码,不是让人看的 保持数据一一对应 否则会报错 下载后,拷贝到一个包里,再 comment是你想添加的注释 txt文本也可

《005.SpringBoot之仓库管理系统》【有文档】

《005.SpringBoot之仓库管理系统》【有文档】 项目简介 [1]本系统涉及到的技术主要如下&#xff1a; 推荐环境配置&#xff1a;IDEA jdk1.8 Maven MySQL8.0 技术栈; 后台&#xff1a;SpringBootMybatisPlus; 前端&#xff1a;thymeleaf; [2]功能模块展示&#xff1a; 1.基础…

相机以及其它传感器传感器

深度相机点云质量对比 比较点云质量时需要注意的点&#xff1a; 1.对特殊材质、颜色的检测效果&#xff1a;透明塑料、金属、毛玻璃、高反光物体&#xff08;镜子、水坑&#xff09;、吸光物体&#xff08;黑色物体&#xff09;。 2.特殊环境&#xff1a;雨、雪、雾、明暗交替位…

RT-DETR算法优化改进:Backbone改进 | HGBlock完美结合PPHGNetV2 RepConv

💡💡💡本文独家改进: PPHGNetV2助力RT-DETRHGBlock与PPHGNetV2 RepConv完美结合 推荐指数:五星 HGBlock_PPHGNetV2 | 亲测在多个数据集能够实现涨点 RT-DETR魔术师专栏介绍: https://blog.csdn.net/m0_63774211/category_12497375.html ✨✨✨魔改创新RT-DETR…

Windows电脑如何一键还原系统?

​如果系统出现故障并且需要将电脑一键还原进行修复&#xff0c;以下是在不同版本的Windows操作系统中一键还原系统的操作步骤。 对于 Windows 10/11&#xff1a; 1.按下键盘上的 "Win I" 组合键&#xff0c;打开设置。 2.在设置中选择 "系统" 选项。…

leetcode刷题日记:118.Pascal‘s Triangle(杨辉三角)

118.Pascal’s Triangle(杨辉三角&#xff09; 题目给我们一个整数numRows表示杨辉三角形的行数&#xff0c;返回杨辉三角形的前numRows行&#xff0c;下面给出一个杨辉三角形看看它有哪些规律&#xff1b; 可以看出杨辉三角形的每一行的最左侧和最右侧的值都为1. 其余的第…

时钟丢失监控机制

文章目录 前言一、分析芯片手册1、6.2.4 Clock monitoring2、28.1.5 System clock and clock monitor requirement3、分析寄存器1) SCG_SOSCCSR[SOSCCM]2) SCG_SOSCCSR[SOSCCMRE] 二、EB配置1、检查复位源2、配置时钟监控 三、结果验证总结 前言 本文章主要基于恩智浦 S32K14x…

Verilog基础:三段式状态机与输出寄存

相关阅读 Verilog基础https://blog.csdn.net/weixin_45791458/category_12263729.html 对于Verilog HDL而言&#xff0c;有限状态机(FSM)是一种重要而强大的模块&#xff0c;常见的有限状态机书写方式可以分为一段式&#xff0c;二段式和三段式&#xff0c;笔者强烈建议使用三…

【Redis】Hash哈希类型

上一篇&#xff1a; set集合 https://blog.csdn.net/m0_67930426/article/details/134366814?spm1001.2014.3001.5502 目录 Hset Hget Hlen Hkeys Hvals Hincrby Hdecrby Hsetex Hsetnx 官网&#xff1a; https://redis.io/commands/?grouphash Hset 创建哈希集…

【Docker】深入理解Docker:一种革新性的容器技术

前言 Docker 是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux或Windows操作系统的机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。 &#x1f4d5;作者简介&#xff1a;热…

游戏本地化翻译,如何确保翻译质量?

游戏本地化翻译是一项颇为复杂的任务&#xff0c;涉及的细节和要求颇多&#xff0c;尤其是需要符合行业特定的规范&#xff0c;才能提升游戏翻译的专业水准。那么&#xff0c;如何确保游戏本地化翻译的品质呢&#xff1f; 业内人士普遍认为&#xff0c;要达到专业水准&#xff…

接口测试面试题整理​​​​​​​

HTTP, HTTPS协议 什么是DNS HTTP协议 怎么抓取HTTPS协议 说出请求接口中常见的返回状态码 http协议请求方式 HTTP和HTTPS协议区别 HTTP和HTTPS实现机有什么不同 POST和GET的区别 HTTP请求报文与响应报文格式 什么是Http协议无状态协议?怎么解决HTTP协议无状态协议 常见的POST提…

论文导读 | 融合大规模语言模型与知识图谱的推理方法

前 言 大规模语言模型在多种自然语言处理相关任务上展现了惊人的能力&#xff0c;如智能问答等&#xff0c;但是其推理能力尚未充分展现。本文首先介绍大模型进行推理的经典方法&#xff0c;然后进一步介绍知识图谱与大模型融合共同进行推理的工作。 文章一&#xff1a;使用思维…
最新文章