实现临界区互斥访问的基本方法

news2024/9/23 13:29:39

1. 问题引入

        在我们之前的生产者与消费者问题中, 在文章的最后, 我们曾尝试过把我们的代码封装成P()和V()操作, 结果却以失败告终. 归根结底是因为我们无法在不使用mutex的情况下来完成对临界区的互斥访问, 本篇文章我们就来探讨一下, 如何不使用mutex实现临界区的互斥访问. 

2. 寻找线程安全的操作

        我们封装P()和V()操作失败的根源在于我们封装出来的方法并不是原子操作. 它们是有可能被打断的, 也就是说它们不是线程安全的代码. 所以我们需要写出线程安全的代码才能实现对临界区的互斥访问. 哪些操作是线程安全的? 之前我们在读者与写者问题中提到过, 读取操作是线程安全的. 除此之外, 还有什么操作是线程安全的? 这里就不卖关子了, 如果一个操作是给整型变量赋值一个确定的值(例如int a = 1; ), 那么这个操作一般是原子的, 因此它也是线程安全的操作. 

3. 临界区互斥访问的基本方法

        1. 轮换法

        我们仍然从最简单的情况说起, 考虑只有两个线程. 回想我们之前的互斥访问的操作, 线程在访问临界区之前, 先看一下锁与钥匙有没有被占用, 如果被占用, 该线程就进入等待状态; 如果没有被占用, 该线程就可以顺利访问临界区. 访问临界区之后, 归还锁与钥匙. 这里我们也可以用同样的思路, 线程访问临界区之前, 读取(这个操作是线程安全的)一个标志位trun, 如果这个标志位允许该线程进入临界区, 那么该线程就可以继续执行; 否则就让这个线程陷入死循环, 即线程进入等待状态. 线程从临界区出来以后, 改变这个标志位的值(这个操作也是线程安全的), 让另一个线程可以从死循环中出来. 比如有两个线程T0和T1, turn == 0时, T0可以进入临界区, turn==1时, T1可以进入临界区. 代码如下: 

#include <iostream>
#include <thread>
#include <windows.h>
#include <condition_variable>
#include <mutex>
#include <chrono>

/// <summary>
/// 标志位, turn == i时, 线程i可以访问临界区
/// </summary>
int turn = 0; 

/// <summary>
/// 临界区代码中的操作数
/// </summary>
int num = 0;

/// <summary>
/// 临界区代码
/// </summary>
void critical_region_fun(int index) {
    printf("线程%d访问临界区, 操作数的值: %d\n", index, ++num);
}

/// <summary>
/// 线程0函数
/// </summary>
void thread_fun0() {
    while (true) {
        while (turn != 0)       //没有轮到自己, 则陷入死循环
        {
            //陷入死循环, 线程进入等待状态
        }

        //访问临界区
        critical_region_fun(0);
        std::this_thread::sleep_for(std::chrono::seconds(1));   //当前线程阻塞1秒

        turn = 1;               //访问临界区的权限交给另外一个线程
    }
}

/// <summary>
/// 线程1函数
/// </summary>
void thread_fun1() {
    while (true) {
        while (turn != 1)       //没有轮到自己, 则陷入死循环
        {
            //陷入死循环, 线程进入等待状态
        }

        //访问临界区
        critical_region_fun(1);
        std::this_thread::sleep_for(std::chrono::seconds(1));   //当前线程阻塞1秒

        turn = 0;               //访问临界区的权限交给另外一个线程
    }
}

int main()
{
    std::thread thread0(thread_fun0);    //线程0
    std::thread thread1(thread_fun1);    //线程1

    thread0.join();    //等待线程结束
    thread1.join();

    return 0;
}

代码1: 轮换法(满足互斥访问, 不满足空闲让进, 不满足有限等待)

        我们在临界区中实现了num的自增操作. 运行结果如下: 

        可以看出线程0与线程1轮流访问临界区, 这就是轮换法名称的由来. 关于轮换法是否可以实现互斥访问, 可以用反证法证明: 假设两个线程同时访问临界区, 则说明两个线程都跳出了死循环, 则两个while的条件均不满足, 即turn == 0 且turn == 1, 矛盾! 故假设不成立. 故两个线程不可能同时访问临界区. 因此轮换法可以保证互斥访问临界区. 那么这段代码还有别的问题吗? 

        我们还是考虑最简单的一种情况: 只有一个线程0, 会发生什么情况呢? 首先turn == 0, 跳过while循环, 然后访问临界区的代码, 然后将turn置为1. 此时由于只有一个线程0, 因此没有其它的线程将turn置为0, 因此线程0就会一直执行while的死循环语句, 再也无法跳出循环. 此时的临界区明明是空闲状态, 线程0却无法访问, 而且如果没有其它的线程, 线程0会这么一直等待下去. 这显然是不合理的. 

        下面我们就来总结一下实现临界区互斥访问的设计原则: 

        互斥访问: 如果有一个进程(线程)处在临界区中, 则其余进程(线程)不能进入. 互斥访问保证协作进程(线程)的正确运行; 

        空闲让进: 多个进程(线程)等待进入临界区时, 只要临界区为空, 应尽快使某一个进程(线程)进入, 空闲让进保证进程(线程)协作高效运行; 

        有限等待: 从进程(线程)发出请求到允许进入, 不能无线等待; 

        让权等待(非必须实现): 进程(线程)不能进入临界区时, 应立即释放处理器, 防止进程(线程)忙等待. 这个不是必须实现的. 比如上面的代码中, 线程0访问临界区时, 线程1一直执行死循环, 占用了处理器, 这个是允许的. 

        可以看出轮换法不满足空闲让进, 也不满足有限等待. 如果只有一个线程0, 且没有外界干预的情况下, 线程0执行完一轮之后, 就永远卡在这了. 

        2. 标记法

        轮换法的问题在于, 只用一个turn来标记可以访问临界区的线程是不够的, 我们需要用多个标记来记录线程是否可以访问临界区, 只要其余线程无法访问临界区, 且当前线程可以访问临界区, 那么当前线程就可以进入临界区执行. 显然可以用一个bool类型的数组flag[ ] 来实现, flag[ i ] 为true代表线程 i 可以访问临界区; flag[ i ] 为false, 代表线程 i 无法访问临界区. 代码如下: 

#include <iostream>
#include <thread>
#include <windows.h>
#include <condition_variable>
#include <mutex>
#include <chrono>

/// <summary>
/// 标志位数组. falg[i] == true时, 线程i可以访问临界区; falg[i] == false时, 线程i无法访问临界区
/// </summary>
bool flag[] = {false, false};

/// <summary>
/// 临界区代码中的操作数
/// </summary>
int num = 0;

/// <summary>
/// 临界区代码
/// </summary>
void critical_region_fun(int index) {
    printf("线程%d访问临界区, 操作数的值: %d\n", index, ++num);
}

/// <summary>
/// 线程0函数
/// </summary>
void thread_fun0() {
    while (true) {
        flag[0] = true;                 //线程0需要访问临界区, 将flag[0]置为true
        while (flag[1] == true)         //没有轮到自己, 则陷入死循环
        {
            //陷入死循环, 线程进入等待状态
        }

        //访问临界区
        critical_region_fun(0);
        std::this_thread::sleep_for(std::chrono::seconds(1));   //当前线程阻塞1秒

        flag[0] = false;                //线程0已结束对临界区的访问, 交出临界区的访问权限.
    }
}

/// <summary>
/// 线程1函数
/// </summary>
void thread_fun1() {
    while (true) {
        flag[1] = true;                 //线程1需要访问临界区, 将flag[1]置为true
        while (flag[0] == true)         //没有轮到自己, 则陷入死循环
        {
            //陷入死循环, 线程进入等待状态
        }

        //访问临界区
        critical_region_fun(1);
        std::this_thread::sleep_for(std::chrono::seconds(1));   //当前线程阻塞1秒

        flag[1] = false;                //线程1已结束对临界区的访问, 交出临界区的访问权限.
    }
}

int main()
{
    std::thread thread0(thread_fun0);    //线程0
    std::thread thread1(thread_fun1);    //线程1

    thread0.join();    //等待线程结束
    thread1.join();

    return 0;
}

代码2: 标志法(满足互斥访问, 不满足空闲让进, 不满足有限等待)

        先看看这段代码能否保证互斥访问, 还是用反证法, 假设线程0和线程1都进入了临界区, 则两个线程均跳出了循环, 则有flag[0] == false且flag[1] == false; 又由两个线程均执行了flag[ i ] = true的操作, 则有flag[0] == true且flag[1] == true, 矛盾! 故假设不成立, 故标志法可以保证互斥访问. 再看只有线程0的情况下, 线程0能否正常运行. 易得flag[1]恒为false, 故线程0不会执行死循环, 故单个线程下可以正常运行. 再看是否满足空闲让进和有限等待. 假设有两个线程, 线程0执行了flag[0] = true; 然后发生调度, 执行线程1的代码flag[1] = true; 然后发生调度, 执行线程0, 此时flag[1] == true; 线程0陷入死循环, 然后发生调度, 执行线程1, 此时flag[0] == true, 线程1也陷入死循环. 此时临界区空闲, 两个线程却都进不来, 而且如果没有外界干预, 两个线程会无限地等待下去. 因此标志法不满足空闲让进, 不满足有限等待

        3. 皮特森(Peterson)算法

        标志法的问题在于, 在判断flag[ i ] 是否为true时, 即使flag[ i ] 为true, 线程i也有可能还没进入临界区, 它甚至连while循环都没进入, 这就有可能导致两个线程都进入了死循环. 因此我们需要有一个标记, 这个标记需要保证两个线程至少有一个可以跳出while循环. 我们可以把轮换法中的turn拿过来, 在线程 i 进入临界区之前, 将turn置为 j , 在while循环的判断里面加上 && turn == j 或者 && turn == i; 这样turn就只有值可以取, 只有就可以保证两个while循环中至少有一个可以跳出. 代码如下: 

#include <iostream>
#include <thread>
#include <windows.h>
#include <condition_variable>
#include <mutex>
#include <chrono>

/// <summary>
/// 标志位数组. falg[i] == true时, 线程i可以访问临界区; falg[i] == false时, 线程i无法访问临界区
/// </summary>
bool flag[] = {false, false};

/// <summary>
/// 标志位, turn == i时, 线程i可以访问临界区
/// </summary>
int turn = 0;

/// <summary>
/// 临界区代码中的操作数
/// </summary>
int num = 0;

/// <summary>
/// 临界区代码
/// </summary>
void critical_region_fun(int index) {
    printf("线程%d访问临界区, 操作数的值: %d\n", index, ++num);
}

/// <summary>
/// 线程0函数
/// </summary>
void thread_fun0() {
    while (true) {
        flag[0] = true;                 //线程0需要访问临界区, 将flag[0]置为true
        turn = 1;                       //线程1可以访问临界区
        while (flag[1] == true && turn == 1)         //没有轮到自己, 则陷入死循环
        {
            //陷入死循环, 线程进入等待状态
        }

        //访问临界区
        critical_region_fun(0);
        std::this_thread::sleep_for(std::chrono::seconds(1));   //当前线程阻塞1秒

        flag[0] = false;                //线程0已结束对临界区的访问, 交出临界区的访问权限.
    }
}

/// <summary>
/// 线程1函数
/// </summary>
void thread_fun1() {
    while (true) {
        flag[1] = true;                 //线程1需要访问临界区, 将flag[1]置为true
        turn = 0;                       //线程0可以访问临界区
        while (flag[0] == true && turn == 0)         //没有轮到自己, 则陷入死循环
        {
            //陷入死循环, 线程进入等待状态
        }

        //访问临界区
        critical_region_fun(1);
        std::this_thread::sleep_for(std::chrono::seconds(1));   //当前线程阻塞1秒

        flag[1] = false;                //线程1已结束对临界区的访问, 交出临界区的访问权限.
    }
}

int main()
{
    std::thread thread0(thread_fun0);    //线程0
    std::thread thread1(thread_fun1);    //线程1

    thread0.join();    //等待线程结束
    thread1.join();

    return 0;
}

代码3: 皮特森(Peterson)算法(满足互斥访问, 满足空闲让进, 满足有限等待)

        先看皮特森算法是否满足互斥访问, 同样使用反证法. 假设两个线程都进入了临界区, 则falg[0]和flag[1]均为true, 且两个while循环都跳过了, 则有turn != 0 且turn != 1, 矛盾! 故假设不成立, 故该算法满足互斥访问. 

        再看是否满足空闲让进. 假设临界区空闲, 即没有线程要访问临界区, 则flag[0]和flag[1]均为false, 故两个while循环的判断条件均为false, 故两个while循环均可以跳出, 可以让新来的线程访问临界区, 因此满足空闲让进. 

        再看是否满足有限等待. 由于turn的取值只有0和1, 因此while循环的判断条件必有一个为false, 因此当两个线程访问临界区时, 必有一个线程 i 可以跳出while循环, 从而访问临界区, 而线程 j 则陷入死循环. 当线程 i 访问完临界区以后, 将flag[ i ]置为false, 则线程 j 的while循环条件不满足, 线程 j 就可以跳出死循环, 进而访问临界区了, 因此满足有限等待. 

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

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

相关文章

形态学操作

目录 1、腐蚀 1.1 腐蚀目的 1.2 原理与代码实现 2、膨胀 3、应用 3.1 开闭运算、形态学梯度 3.1.1 开运算 3.1.2 闭运算 ​编辑 3.1.3 形态学梯度 ​编辑 3.1.4 顶帽与黑帽运算 3.2 相关函数 形态学操作常用于对二值化图像的操作 1、腐蚀 1.1 腐蚀目的 去除图像中…

[工业互联-22]:常见EtherCAT主站方案:Acontis公司的商用Windows 解决方案

目录 前言&#xff1a;非实时、纯软件解决方案 1.1 概述 1.2 缺点 1.3 实时性思路 方案1&#xff1a;非实时性能的解决方案&#xff1a;etherCAT优化网卡驱动程序 方案2&#xff1a;EtherCAT内核调度模块EcatDrv 方案3&#xff1a;具有硬实时性能的解决方案&#xff1a;…

【C++】C++创建动态链接库并调用

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍。 学其所用&#xff0c;用其所学。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&#xff0c;下次更新不迷路&#x1f95e; 文章目…

车载蓝牙通信开发之各种协议原理解析

车载蓝牙开发需要考虑到蓝牙协议栈集成、连接管理、电话功能集成、媒体播放控制、数据交换和服务发现、安全性和隐私保护等方面。这对于实现车辆与蓝牙设备之间的无线通信和交互功能非常关键。 使车辆能够与蓝牙设备进行通信和交互的开发过程。 蓝牙协议栈集成&#xff1a; …

SpringCloud与SpringBoot版本对应关系

浏览器访问start.spring.io/actuator/info 出现JSON字符串&#xff0c;再访问json.cn&#xff0c;在里面解析该字符串 如果对应不上&#xff0c;可能会出现很多环境上的坑

KKRT16 PSI算法

概念介绍 KKRT16 算法是一种基于OT的轻量级隐私求交协议&#xff0c;用于在半诚实敌手存在的情况下对伪随机函数&#xff08;OPRF&#xff09;进行不经意的评估。 在 OPRF 协议中&#xff0c;接收器有一个输入 r r r&#xff1b; 发送方获得输出 s s s&#xff0c;接收方获得…

【Hello mysql】 mysql的基本查询

Mysql专栏&#xff1a;Mysql 本篇博客简介&#xff1a;介绍mysql的基本查询 mysql的基本查询 create单行插入全列插入多行查询指定列查询插入否则更新 &#xff08;不常用&#xff09;替换 Retrieveselect列全列查询指定列查询查询字段为表达式结果去重 where条件找到英语小于6…

Unity 分块延迟渲染01 (TBDR)

现代移动端图形体系结构的概述 现代SoC通常会同时集成CPU和GPU。 CPU被用于处理需要低内存延迟的序列、大量分支的数据集&#xff0c;其晶体管用于流控制和数据缓存。 GPU为处理大型&#xff0c;未分支的数据集&#xff0c;如3D渲染。晶体管专用于寄存器和算术逻辑单元&…

Django_内置的用户认证系统

目录 一、用户对象 1. 创建用户 2. 修改密码 3. 用户验证 二、权限与授权 1. 默认权限 2. 用户组 3. 在代码中创建权限 4. 权限缓存 三、在视图中认证用户 1、登录用户 2、注销用户 3、用户登录的访问限制 3.1、原始的办法 3.2、函数视图使用login_required装饰…

【前端】网页开发精讲与实战 CSS Day 1

&#x1f680;Write In Front&#x1f680; &#x1f4dd;个人主页&#xff1a;令夏二十三 &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd; &#x1f4e3;系列专栏&#xff1a;前端 &#x1f4ac;总结&#xff1a;希望你看完之后&#xff0c;能对你有…

Go 并发模型—Goroutine

前言 Goroutines 是 Go[1] 语言主要的并发原语。它看起来非常像线程&#xff0c;但是相比于线程它的创建和管理成本很低。Go 在运行时将 goroutine 有效地调度到真实的线程上&#xff0c;以避免浪费资源&#xff0c;因此您可以轻松地创建大量的 goroutine&#xff08;例如每个请…

快速排序—C语言实现

目录 前言 快速排序 实现逻辑 1. hoare版本​编辑 2. 挖坑法 3. 前后指针版本 快速排序优化 1. 三数取中法选key 2. 递归到小的子区间时&#xff0c;可以考虑使用插入排序 快速排序非递归&#xff08;用栈实现&#xff09; 快速排序的特性总结 全部代码 前言 &#…

idea-spring boot开发

安装maven与配置配置maven安装插件 已经装好了idea与jdk 安装maven与配置 下载地址: https://maven.apache.org/download.cgi 下载合适的版本 配置maven 打开设置: 直接搜索 :maven 配置变量: 此电脑->属性->高级系统设置->环境变量 新建系统变量 MAVEN_HOME&#xff…

Web安全——渗透测试基础知识下

渗透测试基础 Web安全一、VMware虚拟机学习使用1、虚拟机简单介绍2、网络模式2.1 桥接网络&#xff08;Bridged Networking&#xff09;2.2 NAT模式2.3 Host-Only模式 3、通俗理解 二、Kali的2021安装与配置1、简单介绍2、Kali的版本3、配置3.1 安装虚拟机open-vm-tools-deskto…

基于matlab从ROI和蒙版在图像中创建标记(附源码)

一、前言 此示例演示如何从一组 ROI 创建标记的阻止映像。 在此示例中&#xff0c;您使用两种方法来获取和显示标记的数据。一种方法使用多边形ROI对象来存储肿瘤和正常组织区域边界的坐标。该函数将ROI坐标转换为标记的块图像。第二种方法使用掩码来指示图像的二进制分割为组…

能不能推荐个 vue 后台管理系统模板?

前言 下面是我整理的vue2和vue3的一些后台管理系统模板&#xff0c;希望对你有帮助~ Vue2 1、iview-admin Star: 16.4k 基于 iview组件库开发的一款后台管理系统框架&#xff0c;提供了一系列的强大组件和基础模板&#xff0c;方便开发人员快速搭建一套功能丰富、界面美观、…

web入门案例-部门篇

开发流程 完成对应部门管理和员工管理的需求 准备工作 注意&#xff1a;service还要写接口实体类&#xff0c;mapper只写接口即可&#xff0c;controller是实体类 对应的三个注解 RestController&#xff08;方法返回值作为响应值&#xff09; Mapper(控制反转IOC&#xff0c…

漏洞深度分析 | CVE-2023-36053-Django 表达式拒绝服务

​ 项目介绍 Django 是一个高级 Python Web 框架&#xff0c;鼓励快速开发和简洁、务实的设计。它由经验丰富的开发人员构建&#xff0c;解决了 Web 开发的大部分麻烦&#xff0c;因此您可以专注于编写应用程序&#xff0c;而无需重新发明轮子。它是免费且开源的。 项目地址…

CodeTop整理-树篇

目录 103. 二叉树的锯齿形层次遍历 236. 二叉树的最近公共祖先 124. 二叉树中的最大路径和 102. 二叉树的层序遍历 94. 二叉树的中序遍历 110. 平衡二叉树 572. 另一个树的子树 96. 不同的二叉搜索树 543. 二叉树的直径 297. 二叉树的序列化与反序列化 199. 二叉树的…

eNSP-VRRP虚拟路由器冗余技术

VRRP-虚拟路由器冗余技术 文章目录 VRRP-虚拟路由器冗余技术一、拓扑结构二、基本配置三、测试验证四、知识点详解1.VRRP路由器2.报文格式3.工作过程 一、拓扑结构 二、基本配置 R1: #配置ip <Huawei>sys [Huawei]sys r1 [r1]int g0/0/0 [r1-GigabitEthernet0/0/0]ip a…