操作系统—读者-写者问题及Peterson算法实现

文章目录

  • I.读者-写者问题
    • 1.读者-写者问题和分析
    • 2.读者—写者问题基本解法
    • 3.饥饿现象和解决方案
    • 总结
  • II.Peterson算法实现
    • 1.Peterson算法问题与分析
      • (1).如何无锁访问临界区呢?
      • (2).Peterson算法的基本逻辑
      • (3).写对方/自己进程号的区别是?
    • 2.只包含意向的解决方案
    • 3.严格轮换法
    • 4.完整的Peterson算法
    • 总结
  • 参考资料

I.读者-写者问题

1.读者-写者问题和分析

  • 读者: 读数据
  • 写者: 写数据
  • 访问规则:多个读者可以同时读数据,任何时刻只能有一个写者写数据,一个读者与一个写者不能同时在相应的临界区中
  • 实质: 一个写者不能与其它的读者或写者同时访问相应的临界资源。

  这算是一个非常经典的问题,实际上这也是数据库系统中遇到的最重要的一个问题,因为对于同一个文件,可以多人读,但是不能多人写,如何才能更好地安排资源的分配呢?
  对于这个问题首先肯定应该给出一个基本解法,那就是保证读者可以读,写者在一定情况下也能写,因此比较常用的是这样一种解法:首先有一把排他锁(写锁),只有一个线程可以获得这把排他锁,并且有若干把共享锁(读锁),共享锁可以被任意读线程获取,但是这些线程都不能写,不过如果用信号量实现的话,我们总是需要一个可能无限大的变量进行锁的分配,因此可以改变一下思路:用一个变量记录读者数量,当读者为0时允许获取写锁,而当读者不为0时记录数量,并且不允许获取写锁,因为这个变量本身属于临界区,因此只需要对这个变量加一把互斥锁即可。

2.读者—写者问题基本解法

  在1中,我们已经分析了这个问题的一种基本解法,因此可以用C语言实现如下的代码:

#include <stdio.h>
#include <semaphore.h>
#include <pthread.h>
#define P sem_wait
#define V sem_post
#define RNUMS 10
#define WNUMS 3
sem_t mutex, x;
int readers_cnt;
int rids[RNUMS], wids[WNUMS];void init()
{sem_init(&mutex, 0, 1);sem_init(&x, 0, 1);readers_cnt = 0;for (int i = 0; i < RNUMS; i++) {rids[i] = i + 1;} for (int i = 0; i < WNUMS; i++) {wids[i] = i + 1;} 
}void* Treader(void* ID)
{while (1) {P(&mutex);readers_cnt++;if (readers_cnt == 1) P(&x);V(&mutex);printf("Reader %d: read\n", *(int*)ID);P(&mutex);readers_cnt--;if (readers_cnt == 0) V(&x);V(&mutex);}pthread_exit(NULL);
}void* Twriter(void* ID)
{while (1) {P(&x);printf("Writer %d: write\n", *(int*)ID);V(&x);}pthread_exit(NULL);
}int main()
{init();pthread_t ws[WNUMS], rs[RNUMS];for (int i = 0; i < WNUMS; i++) {pthread_create(&ws[i], NULL, Twriter, &wids[i]);}for (int i = 0; i < RNUMS; i++) {pthread_create(&rs[i], NULL, Treader, &rids[i]);}for (int i = 0; i < WNUMS; i++) {pthread_join(ws[i], NULL);}for (int i = 0; i < RNUMS; i++) {pthread_join(rs[i], NULL);}return 0;
}

  非常简单的实现,对于当前设定的10个读者和3个写者的情况下,观察一段时间输出会发现:几乎根本就不存在写者写入的情况,这是由于读者的锁很明显比排他锁更好获得,对于更易达成的条件,从概率和期望的角度上来说,实现的次数也应该会更多

  这就有问题了:写者想要获取排他锁,看样子非常困难,甚至说,我们可以构造一个读顺序,让写者几乎不可能获取到排他锁:有五个线程,四个读线程和一个写线程,两个读线程为了保障读取到的数据永远是最新的,总是会每隔一分钟读取一次,但是非常巧妙的是,这四个读线程始终有一小段时间是重合的,在这种情况下,因为引用计数一直不能清零,所以排他锁一直不能被获取,此时就造成了写者的饥饿问题

3.饥饿现象和解决方案

  上面已经相对比较详细地描述了饥饿现象的产生,那么一个解决方案是这样的:我既然饥饿现象起源于读者和写者获取锁的难度不公平,那我们就让二者再次公平,在写者全程加上wait_mutex锁,在读者试图增加读者数量的时候也加上wait_mutex锁,因此我们可以写出下面的代码:

#include <stdio.h>
#include <semaphore.h>
#include <pthread.h>
#define P sem_wait
#define V sem_post
#define RNUMS 10
#define WNUMS 1
sem_t mutex, x, wait_mutex;
int readers_cnt;
int rids[RNUMS], wids[WNUMS];void init()
{sem_init(&mutex, 0, 1);sem_init(&x, 0, 1);sem_init(&wait_mutex, 0, 1);readers_cnt = 0;for (int i = 0; i < RNUMS; i++) {rids[i] = i + 1;} for (int i = 0; i < WNUMS; i++) {wids[i] = i + 1;} 
}void* Treader(void* ID)
{while (1) {P(&wait_mutex);P(&mutex);readers_cnt++;if (readers_cnt == 1) P(&x);V(&mutex);V(&wait_mutex);printf("Reader %d: read\n", *(int*)ID);P(&mutex);readers_cnt--;if (readers_cnt == 0) V(&x);V(&mutex);}pthread_exit(NULL);
}void* Twriter(void* ID)
{while (1) {P(&wait_mutex);P(&x);printf("Writer %d: write\n", *(int*)ID);V(&x);V(&wait_mutex);}pthread_exit(NULL);
}int main()
{init();pthread_t ws[WNUMS], rs[RNUMS];for (int i = 0; i < WNUMS; i++) {pthread_create(&ws[i], NULL, Twriter, &wids[i]);}for (int i = 0; i < RNUMS; i++) {pthread_create(&rs[i], NULL, Treader, &rids[i]);}for (int i = 0; i < WNUMS; i++) {pthread_join(ws[i], NULL);}for (int i = 0; i < RNUMS; i++) {pthread_join(rs[i], NULL);}return 0;
}

  在原先代码的基础上简单加上了一个新的wait_mutex作为写者的特权,当写时就会申请wait_mutex作为特权,因此读者和写者在初始状态需要竞争这把互斥锁,在写者竞争到后,读者就无法继续操作,无法增加读者数量,直到写线程的写结束,由此一来发现,由于写者和读者的竞争再次公平,因此写者的写入次数明显提升,对比原先代码的写读比1:10的情况,后者的结果中写入次数明显增加:
在这里插入图片描述

总结

  读者—写者问题是一类非常经典的问题,实际上代表了一系列的文件共享的互斥问题,在数据库系统当中,经常性地查表和写表也属于读者—写者问题的实例,数据库还会采取更多的措施来增加数据库的并发效率,因为目前我们的解决方案上的锁实际上是整个数据库的锁,为了同时让更多读写操作能够进行,数据库采取了表级锁行级锁这些更加细粒度的锁,例如同处在同一个文件的一张表中有两行数据,但是一个事务读取行1,而另一个事务写入行2,这两个操作实际上不会冲突,而采取简单的锁直接锁住会明显降低这个操作的效率

II.Peterson算法实现

1.Peterson算法问题与分析

(1).如何无锁访问临界区呢?

  当两个进程/线程希望访问同一个临界区的时候,应该怎么让这两个线程能够在不发生冲突的情况下获得临界区的数据呢?在Peterson算法之前出现的大部分解决方案实际上都不能完美地解决问题,虽然Peterson算法本身也只能用于解决两个进程之间的互斥问题,但它的确完成了任务。

(2).Peterson算法的基本逻辑

  所以Peterson算法本身究竟做了什么呢?Peterson算法融合了意向严格轮换的想法,对于希望访问临界区的变量,首先它需要将自己的访问意向设置为true,在这之后,有两种方案(本质一样,只是实际意义不同):将自己的进程代号放进turn变量将对方的进程代号放进turn变量,在这两个步骤之后,每个线程就可以去检测对方是否有意向访问 && 目前轮到了对方访问,如果这个条件满足,则需要循环等待。

(3).写对方/自己进程号的区别是?

  所以把对方的和自己的进程号写入turn变量的区别在哪呢?其实比较简单,因为对于同一个变量的写入有一个先来后到的顺序,如果两个进程均写入对方的进程号,则手快的进程会优先把对方进程号写在turn中,手慢的会在自己进程号已经被写入turn之后,把对方的进程号再写入turn当中,这种情况下,进程对于临界区的访问是抢占式的,也就是谁的速度更快,谁就能抢到临界区进行访问。

  而两个进程均写入自己的进程号则是遵循了让步的原则,因为对于上述的情况,手快的进程会优先把自己的进程号写在turn中,手慢的则会在对方进程号已经被写入turn之后,把自己的进程号再写入turn当中,这种情况下,相当于手快的进程把临界区的访问权让给了手慢的进程。

  所以这两种方案其实区别不是很大,只要我们没有一个进程放自己进程号,一个进程放对方进程号,就不会出现很大的问题

2.只包含意向的解决方案

  所以我们知道只包含意向的解决方案是会出问题的,我们可以用下面的代码进行尝试:

#include <stdio.h>
#include <pthread.h>
#include <stdbool.h>
bool flags[2] = {false, false};void* T1()
{while (true) {flags[0] = true;while (flags[1]);printf("T1 access!\n");flags[0] = false; }pthread_exit(NULL);
}void* T2()
{while (true) {flags[1] = true;while (flags[0]);printf("T2 access!\n");flags[1] = false; }pthread_exit(NULL);
}int main()
{pthread_t p1, p2;pthread_create(&p1, NULL, T1, NULL);pthread_create(&p2, NULL, T2, NULL);pthread_join(p1, NULL);pthread_join(p2, NULL);return 0;
}

  非常好代码,T1访问了一次就顺利地进入了死锁的阶段:
在这里插入图片描述
  我用死锁这个词可能都不太好,因为这个解决方案连锁都没有用,开个玩笑。那到底为什么会出这个问题呢?其实很简单,因为我们在等待的时候用了一个完全没法完成互斥的操作—我们只关注对方是不是true,如果true就不访问,那如果两个线程都有意向访问,相当于谁都进不去,换言之,这个方法完全没有解决互斥问题,只是检测是否别人可能在访问罢了。

3.严格轮换法

  这个方法看起来好像要正确一点点,它的代码如下:

#include <stdio.h>
#include <pthread.h>
#include <stdbool.h>
int turn;void* T1()
{while (true) {while (turn != 0);printf("T1 access!\n");turn = 1; }pthread_exit(NULL);
}void* T2()
{while (true) {while (turn != 1);printf("T2 access!\n");turn = 0;}pthread_exit(NULL);
}int main()
{turn = 0;pthread_t p1, p2;pthread_create(&p1, NULL, T1, NULL);pthread_create(&p2, NULL, T2, NULL);pthread_join(p1, NULL);pthread_join(p2, NULL);return 0;
}

在这里插入图片描述

  从结果上看:它好像还真没出问题,严格轮换法的确可以让不同的进程轮流访问临界区,但问题在于,这种方法会在轮不到某个进程的时候让进程持续进入轮询阶段,这会造成CPU的忙等待,浪费了CPU的资源,这种策略之下,快的进程总是不能优先地完成任务,从而造成一定的浪费和调度问题。

4.完整的Peterson算法

  来吧,让竞争更激烈一点。Peterson算法实际上完成了上述两种方法的融合,它的代码实现如下:

#include <stdio.h>
#include <pthread.h>
#include <stdbool.h>
#include <unistd.h>
bool flags[2] = {false, false};
int turn;
int cnt = 0;void* T1()
{while (true) {flags[0] = true;turn = 1;while (flags[1] && turn == 1);sleep(1);printf("T1 access! cnt = %d\n", cnt++);flags[0] = false;}pthread_exit(NULL);
}void* T2()
{while (true) {flags[1] = true;turn = 0;while (flags[0] && turn == 0);printf("T2 access!\n");flags[1] = false;}pthread_exit(NULL);
}int main()
{pthread_t p1, p2;pthread_create(&p1, NULL, T1, NULL);pthread_create(&p2, NULL, T2, NULL);pthread_join(p1, NULL);pthread_join(p2, NULL);return 0;
}

在这里插入图片描述

  结果看起来已经很正确了,它基本完成了Peterson算法的思想,好像问题到这儿就解决了,对吗?这台机器运行在x86-64架构的处理器下,实际上x86-64架构的CPU保证了至少对于int类型的loadstore操作是原子的,如果它们不是原子的,会发生什么?具体的实例我没有查到相关的资料,我也没有成功复现出来,之后我可能还会继续研究一下。

总结

  Peterson算法的确是真正通过软件的方式完成了临界区互斥访问的问题,不过编译器并不一定能够让我们的指令依照顺序执行,编译器可能会对我们写的代码进行乱序,而这样的乱序可能导致load和store指令顺序调整而导致Peterson算法失效,我们可能需要使用:

__sync_synchronize();
// 或者
asm("mfence");

  利用内存屏障指令从而避免对指令顺序进行优化,从而避免出现关键指令乱序执行的问题,所以Peterson算法的实现可能还是需要一些特别的软硬件结合以避免出现乱序的问题。

参考资料

  • 1.并发控制:基础 (Peterson 算法、模型检验、原子操作)
  • 2.博客园—内存栅栏(memory barrier):解救peterson算法的应用陷阱
  • 3.知乎—对int变量赋值的操作是原子的吗?

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

图神经网络实战(7)——图卷积网络(Graph Convolutional Network, GCN)详解与实现

图神经网络实战&#xff08;7&#xff09;——图卷积网络详解与实现 前言1. 图卷积层2. 比较 GCN 和 GNN2.1 数据集分析2.2 实现 GCN 架构 小结系列链接 前言 图卷积网络 (Graph Convolutional Network, GCN) 架构由 Kipf 和 Welling 于 2017 年提出&#xff0c;其理念是创建一…

基于springboot+vue+Mysql的教学视频点播系统

开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;…

idea使用docker将Java项目生成镜像并使用

1&#xff1a;开启docker 远程访问 使用 vim 编辑docker服务配置文件 vim /lib/systemd/system/docker.service [Service] Typenotify # the default is not to use systemd for cgroups because the delegate issues still # exists and systemd currently does not suppor…

操作系统内功篇:内存管理之虚拟内存

一 虚拟内存 在这种情况下&#xff0c;要想在内存中同时运行两个程序是不可能的。如果第一个程序在 2000 的位置写入一个新的值&#xff0c;将会擦掉第二个程序存放在相同位置上的所有内容&#xff0c;所以同时运行两个程序是根本行不通的&#xff0c;这两个程序会立刻崩溃。 …

最简单的 AAC 音频码流解析程序

最简单的 AAC 音频码流解析程序 最简单的 AAC 音频码流解析程序原理源程序运行结果下载链接参考 最简单的 AAC 音频码流解析程序 参考雷霄骅博士的文章&#xff1a;视音频数据处理入门&#xff1a;AAC音频码流解析 本文中的程序是一个AAC码流解析程序。该程序可以从AAC码流中…

Linux(05) Debian 系统修改主机名

查看主机名 方法1&#xff1a;hostname hostname 方法2&#xff1a;cat etc/hostname cat /etc/hostname 如果在创建Linux系统的时候忘记修改主机名&#xff0c;可以采用以下的方式来修改主机名称。 修改主机名 注意&#xff0c;在linux中下划线“_”可能是无效的字符&…

数据结构(初阶)第一节:数据结构概论

本篇文章是对数据结构概念的纯理论介绍&#xff0c;希望系统了解数据结构概念的友友可以看看&#xff0c;对概念要求不高的友友稍做了解后移步下一节&#xff1a; 数据结构&#xff08;初阶&#xff09;第二节&#xff1a;顺序表-CSDN博客 正文 目录 正文 1.数据结构的相关概…

qqqqqqq

欢迎关注博主 Mindtechnist 或加入【Linux C/C/Python社区】一起学习和分享Linux、C、C、Python、Matlab&#xff0c;机器人运动控制、多机器人协作&#xff0c;智能优化算法&#xff0c;滤波估计、多传感器信息融合&#xff0c;机器学习&#xff0c;人工智能等相关领域的知识和…

Polardb MySQL 产品架构及特性

一、产品概述; 1、产品族 参考&#xff1a;https://edu.aliyun.com/course/3121700/lesson/341900000?spma2cwt.28120015.3121700.6.166d71c1wwp2px 2、polardb mysql架构优势 1&#xff09;大容量高弹性&#xff1a;最大支持存储100T&#xff0c;最高超1000核CPU&#xff0…

open Gauss 数据库-03 openGauss数据库维护管理指导手册

发文章是为了证明自己真的掌握了一个知识&#xff0c;同时给他人带来帮助&#xff0c;如有问题&#xff0c;欢迎指正&#xff0c;祝大家万事胜意&#xff01; 目录 前言 openGauss数据库维护管理 1 操作系统参数检查 1.1 实验介绍 1.2 场景设置及操作步骤 2 openGauss 运…

认识什么是Webpack

目录 1. 认识Webpack 1.1. 什么是Webpack?&#xff08;定义&#xff09; 1.2. 使用Webpack 1.2.1. 需求 1.2.2. 步骤 1.3. 入口和出口默认值 1.3.1. 需求代码如下 2. 修改Webpack打包入口和出口 2.1. 步骤&#xff1a; 2.2. 注意 3. Webpack自动生成html文件 3.1.…

D-迷恋网游(遇到过的题,做个笔记)

我的代码&#xff1a; #include <iostream> using namespace std; int main() {int a, b, c; //a表示内向&#xff0c;b表示外向&#xff0c;c表示无所谓cin >> a >> b >> c; //读入数 if (b % 3 0 || 3-b % 3 < c) //如果外向的人能够3人组成…

真·面试题总结——JVM虚拟机

JVM虚拟机 JVM虚拟机规范与实现 JVM虚拟机规范 JVM虚拟机实现 JVM的常见实现 JVM虚拟机物理架构 JVM虚拟机的运转流程 JVM类加载过程 JVM类加载器及类加载器类型 JVM类加载器双亲委派机制 JVM运行时数据区的内存模型 JVM运行时数据区的内存模型&#xff1a;程序计数器…

蓝桥杯第八届c++大学B组详解

目录 1.购物单 2.等差素数列 3.承压计算 4.方格分割 5.日期问题 6.包子凑数 7.全球变暖 8.k倍区间 1.购物单 题目解析&#xff1a;就是将折扣字符串转化为数字&#xff0c;进行相加求和。 #include<iostream> #include<string> #include<cmath> usin…

vue2 列表一般不使用索引删除的原因

在 Vue 中使用索引来删除列表项可能会导致一系列问题&#xff0c;尤其是在处理动态列表时。以下是一些可能的问题和相应的例子&#xff1a; 1. 数据不一致问题 当你使用索引来删除列表中的某个项时&#xff0c;如果列表中的其他项发生了变化&#xff08;比如新增或重新排序&a…

编译时提示存在多个默认构造函数的错误怎么解决呢?

c程序中&#xff0c;如果编译器提升存在多个默认构造函数怎么解决呢&#xff1f; class Date { public:Date(){_year 1900;_month 1;_day 1;}Date(int year 1900, int month 1, int day 1){_year year;_month month;_day day;} private:int _year;int _month;int _day…

chromium源码学习-调试日志 LOG

在学习 chromium 源码时&#xff0c;我们经常需要增加调试日志&#xff0c;常见的用法一般是 LOG(INFO) << "调试信息";其中 INFO 代表当前这条日志的级别&#xff0c;使用的时候就是输入 INFO 就行。接下来我们在探索下这个宏背后的内容。 一、基本用法 LO…

读所罗门的密码笔记08_共生思想(下)

1. 机器判断 1.1. 在生活的各个领域&#xff0c;机器正在我们无意识的情况下做出更多的决定 1.1.1. 我们看到的新闻会塑造我们的观点和行动&#xff0c;它们是根据我们过去行为中所表达的倾向&#xff0c;或者其他同类人的行为而生成的 1.2. …

K-均值聚类算法

K-均值聚类算法是一种常用的无监督学习算法&#xff0c;用于将数据集分成 K 个簇。该算法的主要思想是通过迭代的方式将数据点分配到离它们最近的簇中&#xff0c;并更新簇的中心点&#xff0c;直到满足某个停止条件为止。 以下是 K-均值聚类算法的基本步骤&#xff1a; 初始化…

【热门话题】WebKit架构简介

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 WebKit架构简介一、引言二、WebKit概览1. 起源与发展2. 模块化设计 三、WebCore…