【Linux】线程互斥

文章目录

    • 线程互斥
    • 互斥量 mutex
      • 初始化互斥量
      • 加锁与解锁
    • 可重入和线程安全
      • 常见的线程安全情况
      • 常见的线程安全的情况
      • 常见的不可重入情况
      • 常见的可重入情况
      • 可重入与线程安全联系
      • 可重入与线程安全区别
    • 死锁
      • 死锁的四个必要条件
      • 避免死锁

线程互斥

进程线程间的互斥相关背景概念

  • 临界资源: 多线程执行流共享的资源叫做临界资源。
  • 临界区: 每个线程内部,访问临界资源的代码,就叫做临界区。
  • 互斥: 任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
  • 原子性: 不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

注:临界资源可能会因为多个线程同时访问这块资源导致数据错乱。

#include <iostream>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>
using namespace std;// int 票数计数器
int tickets = 1000; // 临界资源,可能会因为共同访问,造成数据不一致的问题void *getTickets(void *args)
{const char *name = static_cast<const char *>(args);while (true){// 临界区if (tickets > 0){usleep(1000);cout << name << " 抢到了票, 票的编号: " << tickets << endl;tickets--;}else{break;}}return nullptr;
}
int main()
{pthread_t tid1;pthread_t tid2;pthread_t tid3;pthread_t tid4;pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");//线程1pthread_create(&tid2, nullptr, getTickets, (void *)"thread 2");//线程2pthread_create(&tid3, nullptr, getTickets, (void *)"thread 3");//线程3pthread_create(&tid4, nullptr, getTickets, (void *)"thread 4");//线程4pthread_join(tid1,NULL);pthread_join(tid2,NULL);pthread_join(tid3,NULL);pthread_join(tid4,NULL);return 0;
}

可以看到,明明就设置了1000张票,怎么会出现编号为-1、-2的票呢?

这是因为tickets--的操作不是原子性的,而是分为三个步骤:

  1. 将共享变量tickets从内存加载到寄存器中
  2. 更新寄存器里面的值 执行-1操作
  3. 将新值从寄存器写回共享变量tickets的内存地址

汇编代码如下:

既然–操作要经历三个步骤才能完成 那么有可能thread1刚刚把1000读进cpu就被切换了

当该线程被切换的时候会保存它对应的上下文信息 1000这个数据当然也在里面 之后thread1被挂起。

在这里插入图片描述

之后我们的thread2进程就被调度了 因为当thread1被切换的时候内存中的tickek值并没有被改变 所以说thread2看到的值还是1000

我们假设thread2的竞争性比较强 它执行了100次之后才被切换 那么此时的ticket的值就由1000变成了900。

当thread2切换挂起之后我们的thread1回来继续执行 此时恢复它的上下文数据

由于上次保存时它寄存器中的数据是1000 所以说再经历23两步操作之后变为999之后加载到内存中

于是内存中的数据便从900变成999了 相当于此时多了1000张票。

从上面的流程中我们可以看出 --ticket这个操作并不是原子性的

那么我们如何解决上面的问题呢?

其实思路很简单 我们只需要将--ticket这个操作变成原子性的就好了

那么怎么将它变成原子性的呢? 我们的策略是加锁

互斥量 mutex

要解决如上的问题就得做到以下三点:

  • 代码必须有互斥行为:当代码进入临界区执行时 不允许其他线程进入该临界区
  • 如果多个线程同时要求执行临界区的代码 并且此时临界区没有线程在执行 那么只能允许一个线程进入该临界区
  • 如果线程不在临界区中执行 那么该线程不能阻止其他线程进入临界区

我们可以使用如下代码来定义一个互斥量。

pthread_mutex_t mutex;

初始化互斥量

#include <pthread.h>int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  • 返回值:成功返回0,失败返回错误号。

pthread_mutex_init函数对Mutex做初始化,参数attr设定Mutex的属性,如果attrNULL则表示缺省属性。用pthread_mutex_init函数初始化的Mutex可以用pthread_mutex_destroy销毁。如果Mutex变量是静态分配的(全局变量或static变量),也可以用宏定义PTHREAD_MUTEX_INITIALIZER来初始化,相当于用pthread_mutex_init初始化并且attr参数为NULL

加锁与解锁

#include <pthread.h>int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 返回值:成功返回0,失败返回错误号。

一个线程可以调用pthread_mutex_lock获得Mutex,如果这时另一个线程已经调用pthread_mutex_lock获得了该Mutex,则当前线程需要挂起等待,直到另一个线程调用pthread_mutex_unlock释放Mutex,当前线程被唤醒,才能获得该Mutex并继续执行。

如果一个线程既想获得锁,又不想挂起等待,可以调用pthread_mutex_trylock,如果Mutex已经被另一个线程获得,这个函数会失败返回EBUSY,而不会使线程挂起等待。

#include <iostream>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>
using namespace std;// int 票数计数器
int tickets = 1000; // 临界资源,可能会因为共同访问,造成数据不一致的问题
//pthread_mutex_t Mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t Mutex;
void *getTickets(void *args)
{const char *name = static_cast<const char *>(args);while (true){pthread_mutex_lock(&Mutex);// 临界区if (tickets > 0){usleep(100);cout << name << " 抢到了票, 票的编号: " << tickets << endl;tickets--;}else{break;}pthread_mutex_unlock(&Mutex);}return nullptr;
}
int main()
{pthread_mutex_init(&Mutex,NULL);pthread_t tid1;pthread_t tid2;pthread_t tid3;pthread_t tid4;pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");//线程1pthread_create(&tid2, nullptr, getTickets, (void *)"thread 2");//线程2pthread_create(&tid3, nullptr, getTickets, (void *)"thread 3");//线程3pthread_create(&tid4, nullptr, getTickets, (void *)"thread 4");//线程4pthread_join(tid1,NULL);pthread_join(tid2,NULL);pthread_join(tid3,NULL);pthread_join(tid4,NULL);return 0;
}

这样就不会出现票数小于等于0的情况了,但是为什么一直都是thread 4抢到票呢?其他的线程为什么不能抢票?

这是因为锁的钥匙已经被线程4早早地拿走了,其他线程没有钥匙是打不开锁的,就只能在锁区域外等候。

锁是否需要被保护呢?

锁是不许要被保护的,因为从汇编层面保证了加锁是原子性的,争夺锁只有两种可能,要么拿到了,要么没有拿到。

为了实现互斥锁的操作,大多数体系结构都提供了swapexchange指令该指令的作用就是把寄存器和内存单元的数据相交换。

以加锁示例,这是由多态汇编语句执行的,上述%al是寄存器,mutex就是内存中的一个变量。每个线程申请锁时都要执行上述语句,执行步骤如下:

  • movb $0%al)先将al寄存器中的值清0。该动作可以被多个线程同时执行,因为每个线程都有自己的一组寄存器(上下文信息),执行该动作本质上是将自己的al寄存器清0。注意:凡是在寄存器中的数据,全部都是线程的内部上下文!多个线程看起来同时在访问寄存器,但是互不影响。

  • xchgb %almutex)然后用此一条指令交换al寄存器和内存中mutex的值,xchgb是体系结构提供的交换指令,该指令可以完成寄存器和内存单元之间数据的交换。

  • 最后判断al寄存器中的值是否大于0。若大于0则申请锁成功,此时就可以进入临界区访问对应的临界资源;否则申请锁失败需要被挂起等待,直到锁被释放后再次竞争申请锁。

  • 示例:假设内存中有一个变量mutex为1,cpu内部有%al寄存器,我们有threadAthreadB俩线程,

**示例:**现在线程A要开始加锁,执行上述语句。首先(movb $0%al),线程A把0读进al寄存器(清0寄存器),然后执行第二条语句(xchgb %almutex),将al寄存器中的值与内存中mutex的值进行交换。

  • 交换完成后,寄存器al的值为1,内存中mutex的值为0。此时这个过程就是加锁

  • 当线程A争议执行第三条语句if判断时,发生了线程切换(切至线程B),但是线程A要把自己的上下文(1)带走。线程B也要执行加锁动作,同样是第一条语句把0加载到寄存器,清0寄存器。

  • 随后线程B执行第二条语句交换动作,可是mutex的数据先前已经被线程A交换至寄存器,然后保存到线程A的上下文了,现在的mutex为0,而线程B执行交换动作,拿寄存器al的0去换内存中mutex的0。

即使我线程A在执行第一条语句把寄存器清0后就发生了线程切换(切至线程B),线程A保存上下文数据(0),此时线程B执行第一条语句把0写进寄存器,随后线程B执行第二条语句xchgb交换:

此时线程A执行第三条语句if判断失败,只能被挂起等待,线程A只能把自己的上下文数据保存,重新切换至线程B,也就是说我线程B只要不运行,你们其它所有线程都无法申请成功。线程B恢复上下文数据(1)到内存,然后执行第三条语句if成功,返回结果

**注意:**上述xchgb就是申请锁的过程。申请锁是将数据从内存交换到寄存器,本质就是将数据从共享内存变成线程私有。

  • mutex就是内存里的全局变量,被所有线程共享,但是一旦用一条汇编语句将内存的mutex值交换到寄存器,寄存器内部是哪个线程使用,那么此mutex就是哪个线程的上下文数据,那么就意味着交换成功后,其它任何一个线程都不可能再申请锁成功了,因为mutex已经独属于某线程私有了。
  • 这个mutex = 1就如同令牌一般,哪个线程先交换拿到1,那么哪个线程就能申请锁成功,所以加锁是原子的
  • 当线程释放锁时,需要执行以下步骤:
  1. 将内存中的mutex置回1。使得下一个申请锁的线程在执行交换指令后能够得到1,形象地说就是“将锁的钥匙放回去”。
  2. 唤醒等待Mutex的线程。唤醒这些因为申请锁失败而被挂起的线程,让它们继续竞争申请锁。

总结:

  • 在申请锁时本质上就是哪一个线程先执行了交换指令,那么该线程就申请锁成功,因为此时该线程的al寄存器中的值就是1了。而交换指令就只是一条汇编指令,一个线程要么执行了交换指令,要么没有执行交换指令,所以申请锁的过程是原子的。
  • 在线程释放锁时没有将当前线程al寄存器中的值清0,这不会造成影响,因为每次线程在申请锁时都会先将自己al寄存器中的值清0,再执行交换指令。
  • CPU内的寄存器不是被所有的线程共享的,每个线程都有自己的一组寄存器,但内存中的数据是各个线程共享的。申请锁实际就是,把内存中的mutex通过交换指令,原子性的交换到自己的al寄存器中。

可重入和线程安全

  • 线程安全: 多个线程并发同一段代码时 不会出现不同的结果 常见对全局变量或者静态变量进行操作 并且没有锁保护的情况下 会出现线程安全问题
  • 重入: 同一个函数被不同的执行流调用 当前一个流程还没有执行完 就有其他的执行流再次进入 我们称之为重入 一个函数在重入的情况下 运行结果不会出现任何不同或者任何问题 则该函数被称为可重入函数 否则是不可重入函数

常见的线程安全情况

  • 不保护共享变量的函数
  • 函数状态随着被调用 状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

常见的线程安全的情况

  • 每个线程对全局变量或者静态变量只有读取的权限 而没有写入的权限 一般来说这些线程是安全的
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性

常见的不可重入情况

  • 调用了malloc/free函数 因为malloc函数是用全局链表来管理堆的
  • 调用了标准I/O库函数 标准I/O可以的很多实现都是以不可重入的方式使用全局数据结构
  • 可重入函数体内使用了静态的数据结构

常见的可重入情况

  • 不使用全局变量或静态变量
  • 不使用malloc或者new开辟出的空间
  • 不调用不可重入函数
  • 不返回静态或全局数据 所有数据都由函数的调用者提供
  • 使用本地数据或者通过制作全局数据的本地拷贝来保护全局数据

可重入与线程安全联系

  • 函数是可重入的 那就是线程安全的
  • 函数是不可重入的 那就不能由多个线程使用 有可能引发线程安全问题
  • 如果一个函数中有全局变量 那么这个函数既不是线程安全也不是可重入的

可重入与线程安全区别

  • 可重入函数是线程安全函数的一种
  • 线程安全不一定是可重入的 而可重入函数则一定是线程安全的
  • 如果对临界资源的访问加上锁 则这个函数是线程安全的 但如果这个重入函数的锁还未释放则会产生死锁 因此是不可重入的

死锁

一般情况下,如果同一个线程先后两次调用lock,在第二次调用时,由于锁已经被占用,该线程会挂起等待别的线程释放锁,然而锁正是被自己占用着的,该线程又被挂起而没有机会释放锁,因此就永远处于挂起等待状态了,这叫做死锁(Deadlock)。另一种典型的死锁情形是这样:线程A获得了锁1,线程B获得了锁2,这时线程A调用lock试图获得锁2,结果是需要挂起等待线程B释放锁2,而这时线程B也调用lock试图获得锁1,结果是需要挂起等待线程A释放锁1,于是线程A和B都永远处于挂起状态了。

写程序时应该尽量避免同时获得多个锁,如果一定有必要这么做,则有一个原则:如果所有线程在需要多个锁时都按相同的先后顺序(常见的是按Mutex变量的地址顺序)获得锁,则不会出现死锁。比如一个程序中用到锁1、锁2、锁3,它们所对应的Mutex变量的地址是锁1<锁2<锁3,那么所有线程在需要同时获得2个或3个锁时都应该按锁1、锁2、锁3的顺序获得。如果要为所有的锁确定一个先后顺序比较困难,则应该尽量使用pthread_mutex_trylock调用代替pthread_mutex_lock调用,以免死锁。

Case1:因为自己拿了锁,又去申请这把锁,导致死锁了。

#include <iostream>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>
using namespace std;// int 票数计数器
int tickets = 1000; // 临界资源,可能会因为共同访问,造成数据不一致的问题
//pthread_mutex_t Mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t Mutex;
void *getTickets(void *args)
{const char *name = static_cast<const char *>(args);while (true){pthread_mutex_lock(&Mutex);pthread_mutex_lock(&Mutex);// 临界区if (tickets > 0){usleep(100);cout << name << " 抢到了票, 票的编号: " << tickets << endl;tickets--;}else{break;}pthread_mutex_unlock(&Mutex);pthread_mutex_unlock(&Mutex);}return nullptr;
}
int main()
{pthread_mutex_init(&Mutex,NULL);pthread_t tid1;pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");//线程1pthread_join(tid1,NULL);pthread_join(tid2,NULL);pthread_join(tid3,NULL);pthread_join(tid4,NULL);return 0;
}

Case2:互相申请对方的锁。

#include <iostream>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>
using namespace std;// int 票数计数器
int tickets = 1000; // 临界资源,可能会因为共同访问,造成数据不一致的问题
pthread_mutex_t Mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t Mutex1=PTHREAD_MUTEX_INITIALIZER;
void *getTickets(void *args)
{const char *name = static_cast<const char *>(args);while (true){pthread_mutex_lock(&Mutex);sleep(1);pthread_mutex_lock(&Mutex1);// 临界区if (tickets > 0){usleep(100);cout << name << " 抢到了票, 票的编号: " << tickets << endl;tickets--;}else{break;}pthread_mutex_unlock(&Mutex1);pthread_mutex_unlock(&Mutex);}return nullptr;
}void *getTickets1(void *args)
{const char *name = static_cast<const char *>(args);while (true){pthread_mutex_lock(&Mutex1);sleep(1);pthread_mutex_lock(&Mutex);// 临界区if (tickets > 0){usleep(100);cout << name << " 抢到了票, 票的编号: " << tickets << endl;tickets--;}else{break;}pthread_mutex_unlock(&Mutex);pthread_mutex_unlock(&Mutex1);}return nullptr;
}int main()
{pthread_t tid1;pthread_t tid2;pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");//线程1pthread_create(&tid2, nullptr, getTickets1, (void *)"thread 2");//线程2pthread_join(tid1,NULL);pthread_join(tid2,NULL);return 0;
}

死锁的四个必要条件

  • 互斥条件: 一个资源每次只能被一个执行流使用
  • 请求与保持条件: 一个执行流因请求资源而阻塞时 对已获得的资源保持不放
  • 不剥夺条件: 一个执行流已获得的资源 在未使用完之前 不能强行剥夺
  • 循环等待条件: 若干执行流之间形成一种头尾相接的循环等待资源的关系

注意 这是四个必要条件 也就是说四个条件全部满足才能够形成死锁

避免死锁

  • 破坏死锁的四个必要条件。
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

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

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

相关文章

深度学习:欠拟合与过拟合

1 定义 1.1 模型欠拟合 AI模型的欠拟合&#xff08;Underfitting&#xff09;发生在模型未能充分学习训练数据中的模式和结构时&#xff0c;导致它在训练集和验证集上都表现不佳。欠拟合通常是由于模型太过简单&#xff0c;没有足够的能力捕捉到数据的复杂性和细节。 1.2 模型…

API接口测试工具为什么尤其重要

在现代软件开发中&#xff0c;API接口测试工具扮演着关键的角色&#xff0c;连接不同的软件组件&#xff0c;实现数据传递和功能调用。为确保API的可靠性、安全性和性能&#xff0c;此工具成为不可或缺的一部分。本文将介绍API接口测试工具的重要性! 1. 自动化测试的效率 API接…

2023数维杯数学建模C题完整版本

已经完成全部版本&#xff0c;获取请查看文末下方名片 摘要 随着人工智能在多个领域的快速发展&#xff0c;其在文本生成上的应用引起了广泛关注。本研究聚焦于辨识人工智能&#xff08;AI&#xff09;生成文本的基本规则&#xff0c;并探究AI文本的检测及其与人类文本的区分…

哪些软件可以监控电脑(保姆级教程!值得收藏!)

今天了解到了一个软件&#xff0c;真的把我吓到了。 我才知道原来我上班时摸鱼时多么愚蠢的一件事情。原来老板可以通过一些软件轻而易举的知道你用电脑做的所有事情&#xff0c;怪不得我每次摸鱼时老板看我的眼神都不对…… 安装好域之盾软件以后&#xff0c;打开就能监控你使…

Typescript 的 class 类

介绍 1. 类介绍 传统的JavaScript通过函数和基于原型的继承来创建可重用的组件&#xff0c;从ES6开始&#xff0c;JavaScript程序员也可以使用面向对象的方法来创建对象。例如&#xff0c;下列通过class关键词&#xff0c;来声明了一个类&#xff1a;Greeter class Greeter …

SystemV共享内存

一、原理 申请&#xff1a;与共享库类似&#xff0c;OS先在共享区开辟/申请一段共享内存&#xff0c;然后通过页表映射&#xff0c;挂接到进程地址空间&#xff0c;返回这块内存的首地址&#xff0c;使得不同进程能访问同一份资源。 释放&#xff1a;去关联释放共享内存 一个进…

Java智慧工地SaaS管理平台源码:AI/云计算/物联网

智慧工地是指运用信息化手段&#xff0c;围绕施工过程管理&#xff0c;建立互联协同、智能生产、科学管理的施工项目信息化生态圈&#xff0c;并将此数据在虚拟现实环境下与物联网采集到的工程信息进行数据挖掘分析&#xff0c;提供过程趋势预测及专家预案&#xff0c;实现工程…

《网络协议》08. 概念补充

title: 《网络协议》08. 概念补充 date: 2022-10-06 18:33:04 updated: 2023-11-17 10:35:52 categories: 学习记录&#xff1a;网络协议 excerpt: 代理、VPN、CDN、网络爬虫、无线网络、缓存、Cookie & Session、RESTful。 comments: false tags: top_image: /images/back…

Vue3+Vite实现工程化,事件绑定以及修饰符

我们可以使用v-on来监听DOM事件&#xff0c;并在事件触发时执行对应的Vue的Javascript代码。 用法&#xff1a;v-on:click "handler" 或简写为 click "handler"vue中的事件名原生事件名去掉 on 前缀 如:onClick --> clickhandler的值可以是方法事件…

OpenCV图像处理、计算机视觉实战应用

OpenCV图像处理、计算机视觉实战应用 专栏简介一、基于差异模型模板匹配缺陷检测二、基于NCC多角度多目标匹配三、基于zxing多二维码识别四、基于tesseract OCR字符识别 专栏简介 基于OpenCV C分享一些图像处理、计算机视觉实战项目。不定期持续更新&#xff0c;干货满满&…

设置 wsl 桥接模式

一、环境要求 Win10/Win11 专业版&#xff0c;并已安装 Hyper-V 二、具体步骤 打开 Hyper-V 管理器 创建虚拟交换机 WSL Bridge 修改wsl配置文件 .wslconfig .wslconfig 文件所在路径如下&#xff1a; C:\Users\<UserName>\.wslconfig若 .wslconfig 文件不存在&am…

全面揭秘!微信传输助手的用处有哪些!

微信文件传输助手不是真人。它主要是通过服务器和网络技术来完成文件传输功能的。用户可通过微信文件传输助手实现文件在手机到电脑端的快速传输&#xff0c;而不需要其他有线设备。 微信文件传输助手是由微信官方提供的功能&#xff0c;主要用于文件的传输和保存。以下是其主要…

基于STC12C5A60S2系列1T 8051单片机的SPI总线器件数模芯片TLC5615实现数模转换应用

基于STC12C5A60S2系列1T 8051单片的SPI总线器件数模芯片TLC5615实现数模转换应用 STC12C5A60S2系列1T 8051单片机管脚图STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式及配置STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式介绍SPI总线器件数模芯片TLC5615介绍通过按…

数据结构与算法编程题5

从有序表中删除重复元素&#xff0c;使表中所有元素值均不相同。 #include <iostream> using namespace std;typedef int ElemType; #define Maxsize 100 #define OK 1 #define ERROR 0 typedef struct SqList {ElemType data[Maxsize];int length; }SqList;void Init_…

单链表相关面试题--2.反转一个单链表

/* 解题思路&#xff1a; 此题一般常用的方法有两种&#xff0c;三指针翻转法和头插法 1. 三指针翻转法记录连续的三个节点&#xff0c;原地修改节点指向 2. 头插法每一个节点都进行头插 */ // 三个指针翻转的思想完成逆置 struct ListNode* reverseList(struct ListNode* head…

python-opencv 培训课程笔记(1)

python-opencv 培训课程笔记&#xff08;1&#xff09; 博主参加了一次opencv库的培训课程&#xff0c;把课程所学整理成笔记&#xff0c;供大家学习&#xff0c;第一次课程包括如下内容&#xff1a; 1.读取图像 2.保存图像 3.使用opencv库显示图像 4.读取图像为灰度图像 …

PlayCover“模拟器”作弊解决方案

当下的游戏市场&#xff0c;移动游戏已占据了主导地位&#xff0c;但移动端游戏碍于屏幕大小影响操作、性能限制导致卡顿等因素&#xff0c;开始逐步支持多端互通。但仍有一些游戏存在移动端与 PC 端不互通、不支持 PC 端或没有 Mac 版本&#xff0c;导致 Mac 设备体验游戏不方…

【以图会意】操作系统的加载流程

声明&#xff1a;本图为博主方便自己记忆理解&#xff0c;诸多疏漏望请博友理性观看&#xff01;如有错误不足恳请指正。 首先&#xff0c;操作系统是一段程序&#xff0c;他保存在ROM中&#xff0c;在开机时&#xff0c;CPU被激活&#xff0c;首先将IR置为BIOS&#xff08;Bas…

《洛谷深入浅出基础篇》 图的基本应用

什么是图&#xff1f; 我们在生活中学习中能看见很多图&#xff0c;地图&#xff0c;路线图&#xff0c;思维导图等等&#xff0c;它们都有一个特点&#xff0c; 你从中任找一个点&#xff0c;你可以找到&#xff0c;从这个点出发&#xff0c;能够到达什么地方&#xff0c;也…

【C++历练之路】list的重要接口||底层逻辑的三个封装以及模拟实现

W...Y的主页 &#x1f60a; 代码仓库分享&#x1f495; &#x1f354;前言&#xff1a; 在C的世界中&#xff0c;有一种数据结构&#xff0c;它不仅像一个神奇的瑰宝匣&#xff0c;还像一位能够在数据的海洋中航行的智慧舵手。这就是C中的list&#xff0c;一个引人入胜的工具…