操作系统进程线程(三)—进程状态、同步互斥、锁、死锁

news2024/11/26 1:54:31

Linux下同步机制

  • POSIX信号量:可用于进程同步,也可用于线程同步
  • POSIX互斥锁+条件变量:只能用于线程同步。

进程同步的四种方法

临界区

对临界资源进行访问。

同步和互斥

  • 同步:多个进程因为合作产生直接制约关系,使得进程有一定的先后执行关系。
  • 互斥:多个进程在同一时刻只有一个进程能进入临界区。

信号量

信号量表示资源的数量,对应的变量是一个整型(sem)变量。另外,还有两个原子操作的系统调用函数来控制信号量,分别是:

  • P操作:将sem-1,如果sem<0,则线程/进程阻塞,否则继续。
  • V操作:将sem加1,如果sem<=0,唤醒一个等待中的进程/线程,表明V操作不会阻塞。
    图片来源小林coding
    在这里插入图片描述

使用信号量解决生产者—消费者问题

生产者在生成数据之后,放在一个缓冲区中,消费者从缓冲区中读数据进行数据处理,任何时刻,只能有一个生产者或者消费者可以访问缓冲区。

所以需要三个信号量:

  • 互斥信号量mutex:互斥访问缓冲区
  • 资源信号量full:用于消费者询问缓冲区是否有数据,有数据则读取数据,初始化值为0,表示缓冲区一开始为空
  • 资源信号量empty:用于生产者询问缓冲区是否有空位,有空位则生成数据,初始化值为缓冲区大小
#define N 100
typedef int semaphore;
semaphore mutex = 1;
semaphore empty = N;
semaphore full = 0;

void producer() {
   while(TRUE) {
       int item = produce_item();
       down(&empty);
       down(&mutex);
       insert_item(item);
       up(&mutex);
       up(&full);
   }
}

void consumer() {
   while(TRUE) {
       down(&full);
       down(&mutex);
       int item = remove_item();
       consume_item(item);
       up(&mutex);
       up(&empty);
   }
}

不能先down(mutex)再down(empty),不然生产者发现empty=0,就会等待,但是这时候消费者又无法对empty操作,就一直等下去了。

管程

管程把控制的代码独立出来,不仅不容易出错,也使得客户端代码调用更容易。
在一个时刻只能有一个进程使用管程。进程在无法继续执行的时候不能一直占用管程,否则其它进程永远不能使用管程。

经典同步问题

哲学家进餐

方案一

#define N 5

void philosopher(int i) {
    while(TRUE) {
        think();
        take(i);       // 拿起左边的筷子
        take((i+1)%N); // 拿起右边的筷子
        eat();
        put(i);
        put((i+1)%N);
    }
}

问题:如果所有哲学家同时拿起左手筷子,那么就会一起等待右边的筷子,从而导致死锁

方案二

#define N 5
semaphore mutex;
void philosopher(int i) {
    while(TRUE) {
        think();
        P(mutex);
        take(i);       // 拿起左边的筷子
        take((i+1)%N); // 拿起右边的筷子
        eat();
        put(i);
        put((i+1)%N);
        V(mutex);
    }
}

问题:可以解决死锁问题,但是每次进餐只有一个哲学家,效率上来说不是最好的解决方案

方案三

偶数编号的哲学家先拿左边的叉子后拿右边的叉子,奇数编号哲学家先拿右边叉子后拿左边叉子。

#define N 5
semaphore mutex;
void philosopher(int i) {
    while(TRUE) {
        think();
		if(i%2==0){
		  take(i);       // 拿起左边的筷子
          take((i+1)%N); // 拿起右边的筷子
		}
		else if(i%2!=0){
		  take((i+1)%N);       // 拿起左边的筷子
          take(i); // 拿起右边的筷子
		}
      
        eat();
        put(i);
        put((i+1)%N);
      
    }
}

方案四

使用一个数组state来记录每一位哲学家的状态,分别是进餐状态、思考状态、饥饿状态(正在试图拿叉子)
那么,一个哲学家只有在两个邻居没有进餐的时候,才可以进入进餐状态。
代码来源阿秀的学习笔记

#define N 5
#define LEFT (i + N - 1) % N // 左邻居
#define RIGHT (i + 1) % N    // 右邻居
#define THINKING 0
#define HUNGRY   1
#define EATING   2
typedef int semaphore;
int state[N];                // 跟踪每个哲学家的状态
semaphore mutex = 1;         // 临界区的互斥,临界区是 state 数组,对其修改需要互斥
semaphore s[N];              // 每个哲学家一个信号量

void philosopher(int i) {
    while(TRUE) {
        think(i);
        take_two(i);
        eat(i);
        put_two(i);
    }
}

void take_two(int i) {
    down(&mutex);
    state[i] = HUNGRY;
    check(i);
    up(&mutex);
    down(&s[i]); // 只有收到通知之后才可以开始吃,否则会一直等下去
}

void put_two(i) {
    down(&mutex);
    state[i] = THINKING;
    check(LEFT); // 尝试通知左右邻居,自己吃完了,你们可以开始吃了
    check(RIGHT);
    up(&mutex);
}

void eat(int i) {
    down(&mutex);
    state[i] = EATING;
    up(&mutex);
}

// 检查两个邻居是否都没有用餐,如果是的话,就 up(&s[i]),使得 down(&s[i]) 能够得到通知并继续执行
void check(i) {         
    if(state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] !=EATING) {
        state[i] = EATING;
        up(&s[i]);
    }
}

读者写者问题

代码来源阿秀的学习笔记

typedef int semaphore;
semaphore count_mutex = 1;
semaphore data_mutex = 1;
int count = 0;

void reader() {
    while(TRUE) {
        down(&count_mutex);
        count++;
        if(count == 1) down(&data_mutex); // 第一个读者需要对数据进行加锁,防止写进程访问
        up(&count_mutex);
        read();
        down(&count_mutex);
        count--;
        if(count == 0) up(&data_mutex);//最后一个读者要对数据进行解锁,防止写进程无法访问
        up(&count_mutex);
    }
}

void writer() {
    while(TRUE) {
        down(&data_mutex);
        write();
        up(&data_mutex);
    }
}

读写锁

读写锁允许多个线程同时读取共享资源,但是只允许一个线程写入共享资源。读写锁分为共享锁(读锁)和独占锁(写锁)两种类型。写锁优先于读锁。

共享锁

允许多个线程同时获取锁并读取共享资源,但阻止其他线程获取独占锁。在读写锁中,如果某个线程已经获得了共享锁,则其他线程也可以获得共享锁,但不允许其他线程获得独占锁。

独占锁

只允许一个线程获取锁并修改共享资源,其他线程在获取写锁之前必须等待当前线程释放锁。在读写锁中,如果某个线程已经获得了独占锁,则其他线程无法获得任何锁,必须等待当前线程释放锁。

使用场景

一般适用于读取操作频繁,写入操作较少的情况,但是在写入操作频繁的情况下读写锁可能会出现饥饿现象。

示例
#include <iostream>
#include <thread>
#include <mutex>
#include <shared_mutex>
#include <chrono>

using namespace std;

shared_mutex rw_lock;   // 读写锁
int shared_data = 0;    // 共享数据

// 读取共享数据的线程函数
void read_thread(int id)
{
    while (true) {
        // 获取共享锁
        shared_lock<shared_mutex> lock(rw_lock);

        // 读取共享数据
        cout << "thread " << id << " read shared data: " << shared_data << endl;

        // 释放共享锁
        lock.unlock();

        // 等待一段时间
        this_thread::sleep_for(chrono::milliseconds(100));
    }
}

// 修改共享数据的线程函数
void write_thread()
{
    while (true) {
        // 获取独占锁
        unique_lock<shared_mutex> lock(rw_lock);

        // 修改共享数据
        shared_data++;

        // 输出修改后的共享数据
        cout << "write thread write shared data: " << shared_data << endl;

        // 释放独占锁
        lock.unlock();

        // 等待一段时间
        this_thread::sleep_for(chrono::milliseconds(500));
    }
}

int main()
{
    // 创建多个读取共享数据的线程
    thread t1(read_thread, 1);
    thread t2(read_thread, 2);
    thread t3(read_thread, 3);

    // 创建一个修改共享数据的线程
    thread t4(write_thread);

    // 等待所有线程结束
    t1.join();
    t2.join();
    t3.join();
    t4.join();

    return 0;
}

互斥锁

一次只能一个线程拥有互斥锁。互斥锁是在抢锁失败的情况下主动放弃CPU进入水面状态直到锁的状态改变时再换醒。
需要直接把锁交给操作系统管理,所以互斥锁在加锁操作时涉及上下文切换。
互斥锁加锁时间大概在100ns左右,实际上一种可能的实现是先自旋一段时间,自旋时间超过阈值之后再将线程投入睡眠中,因此在并发运算中使用互斥锁得效果可能不亚于使用自旋锁。

条件变量

互斥锁只有两个状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁不足,通常和互斥锁一起使用,以免出现竞态条件。
当条件不满足的时候,线程往往解开相应的互斥锁并阻塞线程然后等待条件发生变化,一旦其它的某个线程改变了条件变量,他将通知相应的条件变量唤醒一个或多个正彼此条件变量阻塞的线程。总的来说互斥锁是线程间互斥的机制,条件变量则是同步机制

自旋锁

如果进程无法取得锁,不会立即放弃CPU,而是一直循环尝试获取锁,直到获取为止。自旋锁一般用于枷锁时间很短的场景,效率比较高。

  • 自旋锁一直占用CPU,如果短时间没有获得锁就会一直自旋,降低CPU效率
  • 递归调用有可能死锁
  • 如果是单CPU并且不可抢占,自旋锁就是空操作
#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>

// 互斥锁实现
std::mutex mtx;

void increment_mutex(int &x) {
    for (int i = 0; i < 1000000; ++i) {
        mtx.lock();
        ++x;
        mtx.unlock();
    }
}

// 自旋锁实现
std::atomic_flag spin_lock = ATOMIC_FLAG_INIT;

void increment_spin(int &x) {
    for (int i = 0; i < 1000000; ++i) {
        while (spin_lock.test_and_set(std::memory_order_acquire)); // 自旋等待
        ++x;
        spin_lock.clear(std::memory_order_release); // 释放自旋锁
    }
}

int main() {
    int x = 0;

    // 使用互斥锁的线程
    std::thread t1(increment_mutex, std::ref(x));
    std::thread t2(increment_mutex, std::ref(x));

    t1.join();
    t2.join();

    std::cout << "互斥锁实现的x的值: " << x << std::endl;//2000000

    x = 0;

    // 使用自旋锁的线程
    std::thread t3(increment_spin, std::ref(x));
    std::thread t4(increment_spin, std::ref(x));

    t3.join();
    t4.join();

    std::cout << "自旋锁实现的x的值: " << x << std::endl;//2000000

    return 0;
}

死锁

什么是死锁

两个或者多个线程相互等待对方数据,死锁会导致程序卡死,如果不解锁将会永远无法进行下去。

产生原因

  • 互斥
  • 不剥夺:进程在所获得的资源未释放之前,不能被其他进程抢走,只能自己释放
  • 请求和保持:进程当前所拥有的资源在请求其它资源的时候,由该进程继续占有
  • 循环等待:存在一种进程资源循环等待链,链中每个进程已获得的资源同时被链中下一个进程所请求。

举例

#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

mutex mtx1, mtx2;

void ThreadA()
{
    mtx1.lock(); // 线程A获取mtx1
    cout << "Thread A obtained mutex 1" << endl;

    // 在获取mtx2之前,先暂停一会儿,让线程B有机会获取mtx2
    this_thread::sleep_for(chrono::milliseconds(100));

    mtx2.lock(); // 尝试获取mtx2,但是已经被线程B获取了,因此线程A将被阻塞

    cout << "Thread A obtained mutex 2" << endl;

    // 完成任务后,释放互斥锁
    mtx2.unlock();
    mtx1.unlock();
}

void ThreadB()
{
    mtx2.lock(); // 线程B获取mtx2
    cout << "Thread B obtained mutex 2" << endl;

    // 在获取mtx1之前,先暂停一会儿,让线程A有机会获取mtx1
    this_thread::sleep_for(chrono::milliseconds(100));

    mtx1.lock(); // 尝试获取mtx1,但是已经被线程A获取了,因此线程B将被阻塞

    cout << "Thread B obtained mutex 1" << endl;

    // 完成任务后,释放互斥锁
    mtx1.unlock();
    mtx2.unlock();
}

int main()
{
    // 创建两个线程,分别运行ThreadA和ThreadB函数
    thread t1(ThreadA);
    thread t2(ThreadB);

    // 等待两个线程执行完毕
    t1.join();
    t2.join();

    return 0;
}

在这个例子中,线程A和线程B都试图获取互斥锁mtx1和mtx2。但是,当线程A获取了mtx1时,它会休眠一段时间,让线程B有机会获取mtx2。当线程B获取了mtx2时,它也会休眠一段时间,让线程A有机会获取mtx1。由于线程A和线程B都需要互斥锁,它们都被阻塞了,导致死锁。

死锁处理方法

鸵鸟策略

假装没有发生问题

死锁检测与死锁恢复

检索到死锁发生的时候就采取措施恢复。使用资源分配图法,包括每种类型一个资源和每种类型多个资源的死锁检测。

死锁恢复

  • 抢占
  • 回滚
  • 杀死进程

死锁预防

  • 破坏互斥条件
  • 破坏请求和保持条件
  • 破坏不剥夺条件
  • 破坏循环等待

死锁避免

在程序运行的时候避免发生死锁

安全状态

如果没有死锁发生,并且及时所有进程突然对请求对资源的最大需求,也仍然存在某种次序能够使得每一个进程运行完毕,则就是安全的。

银行家算法

阿秀的学习笔记

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

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

相关文章

教你如何正确使用ChatGPT

目录 前言 一、ChatGPT Sidebar 二、免费镜像 三、共享账号 总结 前言 ChatGPT 是一种基于深度学习技术的自然语言处理工具&#xff0c;能够用于文本生成、语言翻译等任务。然而&#xff0c;其使用需要一定的技术基础和相关知识&#xff0c;不少用户可能会遇到一些问题。…

从功能到自动化,4个月时间我是如何从点工进入互联网大厂的

1、知识体系化 不知不觉&#xff0c;入行软件测试也有五个年头。待过创业公司也待过上市公司。做过功能测试、自动化测试也做过性能测试。做过测试新人也做过测试组长。如果要是从这5年中说出最宝贵的经验&#xff0c;我想应该是知识体系化。那么什么是知识体系化&#xff0c;…

SViT 实验记录

目录 一、网络的搭建 1、Conv Stem 2、各阶段的模块 3、3X3卷积 二、前向传播过程 1、Stem 2、各阶段中的基本模块STT Block 1&#xff09;CPE模块 2&#xff09;STA模块 网络结构 一、网络的搭建 论文中的结构原图 基本模块 1、Conv Stem (patch_embed): PatchEmbed…

算法修炼之练气篇——练气十三层

博主&#xff1a;命运之光 专栏&#xff1a;算法修炼之练气篇 目录 题目 1023: [编程入门]选择排序 题目描述 输入格式 输出格式 样例输入 样例输出 题目 1065: 二级C语言-最小绝对值 题目描述 输入格式 输出格式 样例输入 样例输出 题目 1021: [编程入门]迭代法求…

【Selenium上】——全栈开发——如桃花来

目录索引 Selenium是什么&#xff1a;下载和配置环境变量&#xff1a;1. 基本使用&#xff1a;导入五个常用包&#xff1a;基本代码&#xff1a; 实例引入&#xff1a;声明不同浏览器对象&#xff1a;访问页面&#xff1a; Selenium是什么&#xff1a; Selenium是一个用于Web应…

Cesium入门之四:基于Vue3+Vite+Cesium构建三维地球场景

Cesium官网中提供了基于webpack配置Cesium的方法&#xff0c;但是这种方法太繁琐&#xff0c;而且使用webpack时程序启动没有Vite启动快&#xff0c;因此&#xff0c;这里选择vite创建vue3cesium构建项目 创建vue3项目 新建CesiumProject文件夹&#xff0c;在该文件夹上点击右…

clang-format configurator - 交互式创建 clang-format 格式配置文件

clang-format configurator - 交互式创建 clang-format 格式配置文件 clang-format configurator https://zed0.co.uk/clang-format-configurator/ clang-format-configurator https://github.com/zed0/clang-format-configurator Interactively create a clang-format confi…

minikube,搭建+镜像加速,坚持 3 分钟,带你玩的明明白白

一、 安装 cri-docker 下载安装 # 在 https://github.com/Mirantis/ 下载 https://github.com/Mirantis/tar -xvf cri-dockerd-0.3.1.amd64.tgzcp cri-dockerd/cri-dockerd /usr/bin/chmod x /usr/bin/cri-dockerd# 确认已安装版本 cri-dockerd --version配置启动文件 cri-do…

一篇让你精通JWT,妥妥的避坑指南~

视频教程传送门&#xff1a;JWT 两小时极简入门&#xff1a;JWT实战应用与防坑指南~_哔哩哔哩_bilibiliJWT 两小时极简入门&#xff1a;JWT实战应用与防坑指南~共计12条视频&#xff0c;包括&#xff1a;01.课程介绍与前置知识点、02.JWT概念、03.JWT组成等&#xff0c;UP主更多…

一个例子让你彻底弄懂分布式系统的CAP理论

1 推荐的文章 下面这篇知乎文章是我见过的最简单易懂的一篇&#xff0c;把CAP定义以及为什么AP和CP只能二选一以及场景特定下选AP还是CP作为系统目标等讲解明明白白 谈谈分布式系统的CAP 2 个人对上面这篇文章的的一些补充 可用性可以人为设置一个阈值&#xff0c;比如用户体…

openPOWERLINK源码(最新)在stm32单片机上的移植指南

最近着了powerlink的道&#xff0c;连续几晚十二点前没睡过觉。不得不说兴趣这东西劲太大了&#xff0c;让人睡不着。喜欢上研究POWERLINK&#xff0c;最新版的源码结构挺清晰的&#xff0c;移植并测试了嵌入式linux作为从站和电脑主站之间的通信&#xff0c;挺有趣的。接下来想…

路由器配置方法(固定地址)

前言 由于学校给分配了IP地址&#xff0c;因此我们的路由器接入的时候不能选择自动接入方式&#xff0c;而要选择固定地址方式。 step 1 我们首先先将路由器接上网线&#xff0c;这里注意一定要接wan口 因为路由器分为两个口&#xff0c;wan口是入口&#xff0c;lan口是出口…

第十二届蓝桥杯青少组国赛Python真题,包含答案

第十二届蓝桥杯青少组国赛Python真题 一、选择题 第 1 题 单选题 设sHi LanQiao&#xff0c;运行以下哪个选项代码可以输出“LanQiao”子串 () 答案&#xff1a;A 第 2 题 单选题 已知a-2021.0529&#xff0c;运行以下哪个选项代码可以输出“2021.05”() 答案&#xff1a;…

2023.05.12 C高级 day4

有m1.txt m2.txt m3.txt m4.txt&#xff0c;分别创建出对应的目录&#xff0c;m1 m2 m3 m4 并把文件移动到对应的目录下 #!/bin/bash for i in 1 2 3 4 dotouch m$i.txtmkdir m$imv m$i.txt ./m$i/m$i.txt done 运行结果 2. 使用break关键字打印九九乘法表&#xff0c;提示&am…

【2023/05/12】Z3

Hello&#xff01;大家好&#xff0c;我是霜淮子&#xff0c;2023倒计时第7天。 Share Listen,my heart,to the whispers of the world with which it makes love to you. 译文&#xff1a; 静静的听&#xff0c;我的心呀&#xff0c;听那世界的低语&#xff0c;这是它对你求…

黑客必备工具:Hydra的完整安装和使用指南

安装Hydra 1.安装必要的依赖库 在终端中执行以下命令&#xff0c;安装Hydra所需的依赖库&#xff1a; sudo apt-get install build-essential checkinstall libssl-dev libssh-dev libidn11-dev libpcre3-dev libgtk2.0-dev libmysqlclient-dev libpq-dev libsvn-dev firebi…

经典HTML前端面试题总结

经典HTML前端面试题总结 1. 1简述一下你对 HTML 语义化的理解&#xff1f;.1.2 标签上 title 与 alt 属性的区别是什么&#xff1f;1.3 iframe的优缺点&#xff1f;1.4 href 与 src&#xff1f;1.5 HTML、XHTML、XML有什么区别1.6 知道img的srcset的作用是什么&#xff1f;1.7 …

代码随想录算法训练营第五十九天

代码随想录算法训练营第五十九天| 503.下一个更大元素II&#xff0c;42. 接雨水 503.下一个更大元素II42. 接雨水复杂单调栈整合单调栈 503.下一个更大元素II 题目链接&#xff1a;下一个更大元素II 因为可以循环&#xff0c;直接拼一个nums在nums后面就行了。 class Solutio…

[OGeek2019]babyrop

小白垃圾笔记不建议阅读。。。。 这道题额………………做了好几天。。 更离谱的是还把ubuntu16给玩坏了。 师傅说kali可以打通&#xff0c;气得我连夜下卡里 后来发现不是版本的问题&#xff0c;是我的脚本的问题。libc写的不对。 先分析这道题。 32位程序。没有canary&…

串口与wifi模块

经过以下学习&#xff0c;我们掌握&#xff1a; AT指令与wifi模块的测试方法&#xff1a;通过CH340直接测试&#xff0c;研究各种AT指令下wifi模块的响应信息形式。编程&#xff0c;使用串口中断接收wifi模块对AT指令的响应信息以及透传数据&#xff0c;通过判断提高指令执行的…