打通任督二脉:4000字,一文,从代码拆到计算机底层。

简介: 一个业务场景假设我们要编写一个购买商品的程序,程序的内容很简单:商品的库存数量存放在数据库中,每次完成库存数量-1。我们很快可以写出伪代码

一个业务场景

假设我们要编写一个购买商品的程序,程序的内容很简单:

商品的库存数量存放在数据库中,每次完成库存数量-1。我们很快可以写出伪代码

20201029230537804.png

假设当前商品的库存数量为10,A、B两个用户同时购买商品,正常情况应该如下:


A用户购买:


        a_1.去数据库中读取当前库存值(10)。


        a_2将库存值减去1(10-1=9),将数据库中存放的库存值修改为计算结果(9)。


B用户购买:


        b_1.去数据库中读取当前库存值(9)。


        b_2.将库存值减去1(9-1=8),将数据库中存放的库存值修改为计算结果(9)。


一切看起来都很自然,然而还存在另外一种情况:


        A用户刚刚执行完第1步,第2步还未执行,B用户就执行了第一步,那么会发生什么情况?

20201029230941834.png

是的,两次购买操作以后库存量数据是9。这种情况称为 “数据一致性问题”或者“脏数据”。

那么造成这种情况的原因到底是什么喃?接下来我们将从这个业务情景出发,一步步拆解到计算机底层。准备好了吗?系好安全带,我们发车!

一些概念

要搞明白这个问题的根本原因,需要我们拥有一些关于“计算机组成原理”和“操作系统”的知识。首先在引出具体概念之前,笔者觉得有读者有必要搭建起一个“计算机硬件组成”和“操作系统”之间的一个关系:

“计算机组成原理”阐述了计算机的硬件组成、体系架构是什么样的,“操作系统”则是位于硬件和用户之间的中间层软件,面向用户提供用户接口,方便用户控制计算机,面向计算机,在硬件层上提供一套资源调度策略,负责完成资源调度管理。

“计算机组成原理”相关的一些知识:

CPU

程序本质上就是一段段的指令,命令计算机去完成指定操作,CPU就负责来执行这些指令。对于单个CPU而言是不能一心而用的,同一时间只能执行一段指令,不能执行多段指令。就像你不可能同时左手画圆,右手画方一样。

CPU的态

CPU负责处理、执行指令,指令中有一些系统级别的特权指令,此类指令需要极高权限,如:

20201029231303189.png

因此根据执行指令的权限级别不同(往往高权限也需要更多资源),CPU会呈现出两种状态:


        a) 核态(kernel mode)


                 能够访问所有资源、执行所有指令。用来处理特权指令。


        b) 用户态(user mode,也称“目态”)


                 仅能够访问部分资源,其他资源受限。用来处理普通用户程序编译出来的普通机器指令。


CPU的态,是硬件层面为了提高性能而推出的一个标准,至于为什么?不妨来想一下,要CPU能实时访问所有资源,是不是意味着CPU要一直“眼观六路,耳听八方”?CPU是不是要一直处于一个满血工作的状态,它累不累?会不会短命?功耗大不大?CPU的进行态之间的切换,就像人做热身运动一样,是个十分耗时的过程。


一些需要操作系统资源的代码(比如操作进程和线程),底层就是调用的特权指令,运行这些代码就需要CPU进行态之间的切换。


“操作系统”相关的一些知识:

程序

程序是存放在存储(硬盘)中的一段代码,本质上是一段指令,是长期存在的。

进程

进程,很多时候也被称为“任务”,是程序在某个数据集合上的一次运行活动,是短期存在的。可以理解为被激活,调入内存的一段代码。进程是计算机进行资源分配和调度的基本单位,也就是说CPU是按照进程为单位来执行指令(代码)的。


由于程序可能被多次反复运行,所以一个程序可能存在多个进程。


a).进程的结构:

20201029231731270.png

进程控制块(Process Control Block,PCB):描述进程状态、资源以及和其他进程之间关系的一个数据结构。PCB在创建进程时被创建,进程撤销时同时被撤销。PCB是操作系统管理进程的单位。


      b).进程的状态:


      理论上进程分为三种状态(各个操作系统具体实现上有差异):


      就绪态:已经准备好运行,等待被CPU分配“时间片”。


      运行态:得到“时间片”,开始正常运行。


      阻塞态:由于一些原因造成进程无法继续运行。

20201029231758298.png

   进程会因为一些原因产生状态的跃迁(改变):


                                   ①就绪—>运行:得到时间片。


                                   ②运行—>就绪:时间片到时。


                                   ③运行—>阻塞:运行中争抢某些必要的资源失败或者等待某个信号的来临。


                                   ④阻塞—>就绪:需要的资源被之前争抢到的进程使用完毕后释放或者信号的到来

分时操作系统

        操作系统既然是种策略,那么策略的实现自然就是多种多样的,其核心点的不同主要体现在CPU执行指令的方式上。分时操作系统是当前最主流、使用最广泛的一种实现,其核心为:

        计算机以很短的“时间片”为单位,把CPU轮流分配给每个进程来使用,由于“时间片”被切分的很小,每个进程都感觉自己好像独占了CPU,因此在使用上会有种指令被并发执行的感觉,其实本质上是“伪并发”。

       20201029232419942.png

线程

分时系统”,是为了防止单个进程过大,执行时间过长,从而在架构上通过将CPU资源切成时间片,已经实现多任务交叉执行,且由于时间片分的足够小,让使用者在感官上有一种程序被并发执行的感觉。到这一步,其实计算机的资源已经得到较为充分的利用,但仅仅是“分时”,粒度还是过大,对系统资源仍然存在着浪费,仍然存在着优化的空间。


浪费的具体原因如下:


        例如某个进程得到时间片,在该时间片内,执行的内容如下:

20201029232442339.png

由于进程具有原子性,这组操作在在执行过程中不可分割,第一行代码调用IO设备后,即使已经完成使用,后续不再需要此资源,但是在第N行代码执行结束前,IO设备仍然被该进程所占有。单CPU系统下并不会有问题,因为单CPU通过“分时”实现的“伪并发”在当前时间片执行完成前,其他进程也不会被执行。但是在多CPU系统中,程序则是真正以“并发”的方式被执行的,这种由于资源被单个进程所持有,其他进程无法使用的情况,就造成了资源的浪费。线程的出现是为了在更细的粒度上优化计算机的资源利用,解决此类问题。


线程是为了提高并发性而出现的,又称为轻量级进程,是程序执行的最小单元,是进程中的一个实体,是被系统独立调度和分配的基本单位。同一进程的线程共享该进程的资源,即电脑所分配的资源空间。


上一情景中,可以用线程优化为:


两个单独的线程各自去争抢需要的资源,使用完毕后,结束线程即可,让给其他需要此资源的时间片,不必等到整个时间片执行完毕。

20201029232647454.png

原因解析

前文说了,一堆理论概念,可能已经绕晕了很多小伙伴,没关系,我们现在就来把这些看起来零碎的概念串起来,从计算机的底层来看造成这个问题的原因到底是什么。前文的概念一次阅读没理顺也没关系,只是参考资料罢了,在接下来的解析过程中,可以随时倒回去查对应的概念。

好的,那么我们开始!

我们回看出现前文写的程序,以及出现数据一致性问题的过程简图:    

20201029232737312.png

2020102923274965.png


A、B用户分别购买商品,本质上其实是将存在硬盘上的购买商品的程序代码调入内存中,购买商品的程序产生出两个进程——A进程、B进程。


接下来有两个因素可能造成A进程执行步骤1和步骤2之间,B进程插足执行了步骤1。


1.时间片过小


时间片分的过小,A进程的步骤1和步骤2没有在一个时间片中被连续执行完,中间间隔的时间片被分配给了B进程。


2.多CPU系统造成的纯并发问题


A进程的1、2两步在能一个时间片中被执行完毕,但是程序是在一个多CPU系统中被执行,进程B并为插足执行,而就是单纯的在1、2两行的执行间隙去读取了数据。


也许很多小伙伴会疑惑,不是说进程是具有原子性的嘛,那为什么那为什么在A进程执行过程中A中所用到的资源——库存数量,B进程还可以使用?


这里要注意,在计算机中的读操作都不是直接去持有存储数据的内存,而是拷贝一份副本走。


也许还有小伙伴在思考,线程在这里面扮演了什么角色?其实不难想明白,进程层面都会出现的问题,线程层面那当然更可能出现了!至于是多进程引起的“脏数据”,还是多线程引起的“脏数据”,区别无非就是你是在一个只支持进程的计算机中运行的程序?还是在一个支持线程的计算机中运行的程序?


好的,总结一下,造成“脏数据”的罪魁祸首其实并发(“分时”其实逻辑上也是向并发靠拢)。


那么怎么解决这个问题,保证“数据一致性”喃?解决的方法有很多种,不过参照的理念其实都是相同的,接下来就为大家介绍这个问题的解决思路。

解决方法

临界区

临界资源:一次只允许一个进程单独访问(使用)的资源。

临界区:进程中访问临界资源的程序片段。

临界区是一种理念,本质上是为保证并发环境下数据一致性,其核心思想为:


将各进程数据操作部分做成“封闭模块”。这些“封闭模块”具有原子性,在各程序执行时同一时间内,所有“封闭模块”中只能一个有正在被执行,也就是说“封闭模块”只能被执行,不能并行执行。这些“封闭模块”称为临界区。临界区中需要的资源称为“临界资源”。

20201029232943450.png

锁是临界区的具体实现。

具体实现为:

为临界资源设置一个标志位,进入临界区前检查此标志位。若“可用”则访问且修改为“不可用”,退出临界区后修改为“可用”;若“不可用”则等待。

20201029233015466.png

    “锁”是实现临界区思想的一种实现,而“锁”的实现有两种,一种是“悲观锁”,一种是“乐观锁”。

悲观锁

悲观锁,即预计数据大概率会遇到并发问题,对局势十分悲观,因此直接使用操作进程、线程级别的手段来加锁,检查标志位“可用”则访问,不可用则直接修改进程(线程)状态为阻塞或等待。

20201029233109641.png

这里我们可以看到,“悲观锁”会涉及到进程状态的修改,前文介绍过,操作进程或者线程因为要用到很多系统资源,底层会调用特权指令,由于调用特权指令会造成CPU态之间的切换,加锁、开锁整个过程是十分耗费时间的。因此“悲观锁”又被称为“重锁”。


悲观锁的代表有很多,比如JAVA中的“synchronized”、关系型数据库的“行锁”、“表锁”。

乐观锁

乐观锁,即预计数据大概率不会遇到并发问题,对局势十分乐观。当然乐观,并不意味着不做处理,而是说不用进程(线程)级别的重手段来处理。

乐观锁的处理方式是给每个数据一个版本号,每次操作都更改一次版本号,只要比对版本号就能知道数据是否被自己以外的进程(线程)更改过。


        如果数据更改过,则舍弃根据之前数据运算出来的结果,重新获取当前数据再掉头回去重新运算一次。


        这样一个循环的过程被形象的称为“自旋锁”,也叫CAS。

20201029233327123.png

乐观锁的代表也有很多,比如redis中的锁,springcloud中的ribbon。

总结

总的一句话来说造成“脏数据”,是由计算机底层的调度造成的,并不只局限于本文中所举例的一个业务场景中,很多情况都可能产生“脏数据”,这是不可避免的,但我们可以在代码层面解决它,解决的核心思路是使用“临界区”理念的落地实现——“锁”,锁的实现方式有多种,既有在系统层面进行控制的“悲观锁”,也有在代码层面进行控制的“乐观锁”。


世界上第一位计算机科学的博士,David John Wheeler说过:“计算机科学中的所有问题都可以通过添加中间层来解决,只是这个新的中间层又会引起新的问题。”


引入锁机制来解决并发问题,本质上其实就是引入了一个进行访问控制的中间层,而这个中间层则会带来“同步”、“死锁”等问题。


“同步”问题需要我们使用信号灯机制(PV操作)去解决,“死锁”则需要我们通过破坏环路条件去解决。这两块展开来内容又是车载斗量,所以这里就不做讨论了。笔者最后想说,现在随着“知识爆炸”计算机行业的迭代越来越快,但计算机发展了这么多年核心课程依旧是那几科,乱花渐欲迷人眼,浅草才能没马蹄。


        程序=算法+数据结构


        程序!=业务+框架


笔者学识有限,文中涉及的内容太宽,太广,可能有错误和不到位之处,望诸君包涵,斧正。

目录
相关文章
|
7月前
|
存储
探索计算机内部的神秘语言:二进制的魅力
二进制是一种由0和1组成的数制系统,是计算机中最基础的表示方式。通过了解二进制,我们可以深入了解计算机的内部工作原理,如数据存储、运算和传输等。这种简单而神奇的数字语言将帮助读者揭开计算机世界的神秘面纱,激发对科技的兴趣和探索欲望。
244 2
探索计算机内部的神秘语言:二进制的魅力
|
存储 算法 调度
【考研必备】解开“黑匣子”的神秘面纱,透视数字世界底层实现过程(计算机组成原理)(下)
【考研必备】解开“黑匣子”的神秘面纱,透视数字世界底层实现过程(计算机组成原理)
|
4月前
深挖计算机的根:汇编语言与计算机架构之间不可告人的秘密
【8月更文挑战第31天】本文深入探讨了汇编语言与计算机架构之间的重要联系。通过解析汇编语言的基本概念及其与硬件的直接映射关系,文章展示了它在计算机体系中的独特地位。以一个简单的“Hello, World!”汇编程序为例,详细说明了汇编语言如何操作底层硬件。尽管现代软件开发中较少使用汇编语言,但掌握它有助于理解计算机工作原理,对于性能优化和系统编程至关重要。
52 2
|
存储 安全 网络安全
【考研必备二】解开“黑匣子”的神秘面纱,透视数字世界底层实现过程(计算机组成原理)(下)
【考研必备二】解开“黑匣子”的神秘面纱,透视数字世界底层实现过程(计算机组成原理)
|
7月前
|
缓存 编译器 C语言
|
运维 前端开发 安全
万字长文搞懂产品模式和项目模式
万字长文搞懂产品模式和项目模式
189 0
|
7月前
|
人工智能 自然语言处理 算法
数字永生源码独立部署怎么做?
数字人源码,数字人永生,数字人源码独立部署,数字人
数字永生源码独立部署怎么做?
|
存储 机器学习/深度学习 Unix
【考研必备】解开“黑匣子”的神秘面纱,透视数字世界底层实现过程(计算机组成原理)(上)
【考研必备】解开“黑匣子”的神秘面纱,透视数字世界底层实现过程(计算机组成原理)
【考研必备】解开“黑匣子”的神秘面纱,透视数字世界底层实现过程(计算机组成原理)(上)
|
存储 Unix Linux
【考研必备二】解开“黑匣子”的神秘面纱,透视数字世界底层实现过程(计算机组成原理)(上)
【考研必备二】解开“黑匣子”的神秘面纱,透视数字世界底层实现过程(计算机组成原理)(上)
【考研必备二】解开“黑匣子”的神秘面纱,透视数字世界底层实现过程(计算机组成原理)(上)
|
人工智能 安全 双11
计算机中那些事儿(四):我眼中的虚拟技术
计算机中那些事儿(四):我眼中的虚拟技术