思考:如何学习一个程序

Posted by attack204 on 2022-07-08

究竟什么是一个程序?

我的理解:任何一个程序,本质上都是数据处理程序

为了实现数据处理,一个程序需要实现三个功能通信、计算、存储。

通信

从通信的对象来看有

  • 线程间通信:openGauss采用信号量

  • 进程间通信:共享内存

  • 不同机器之间通信

  1. 前后端交互走HTTP协议
  2. 微服务调用使用RPC,底层还是socket
  3. JDBC连接MySQL时走MySQL协议,连接Hive时走Hive协议,其实底层还是socket

从通信的方式来看有

  • 直接通信

所有程序之间的通信都有两种方法,一种是pull(数据在response中)一种是push(数据在body中)。

例如在raft中如何确定leader的状态?

push:leader定期向follow发送heartBeat,refresh其electionElapsed(相当于leader向follow传数据)

pull:follow定期向leader发送heartBeat,check其有没有在线(相当于follow从leader拉数据)

  • 间接通信

最常见的应该是生产者消费者模型

生产者生产数据打到Broker(Broker中维护了消息队列Queue)

消费者从Broker拉数据

计算

  1. 并行计算问题
  2. 高性能计算问题

存储

从生命周期来看程序

显然,从OS被载入内存,到系统shutdown,OS以及被OS所包含的所有软件都有initialize -> running -> shutdown这三个状态。

因此,如果按这个道理来看

从OS -> 程序 -> 进程 -> 线程 -> 组件 -> 函数 -> 变量

各个维度来看,任何一样东西都会有生命周期。

因此,在学习一个陌生知识点的时候,需要不断的问自己这三个问题:

  1. 这个组件什么时候初始化,如何初始化,具体哪些变量或者数据结构初始化?

  2. 这个组件运行的时候要做什么,上游来的请求是什么样的,偷传给下游的数据是什么样的?

  3. 这个组件什么时候被销毁?具体销毁的代码是怎么写的呢?

OO的本质

new对象的本质是什么?申请一块内存,填充对应的数据

什么是对象

一个对象的本质是什么?是存储 + 计算,因此我们在描述某个对象的功能时,一定可以描述为:xx对象记录了xx信息,提供了xxx的方法。由此也可以反推出,任何一个对象,如果没有理解清楚其记录的信息以及提供的方法,那么对这个对象的理解就是不透彻的。

属性

这里有一个很有意思的问题:属性重要还是方法重要?

对于不同的组件来说不一样

  • 有的对象方法比属性重要

为什么呢?因为任何需求都可以被抽象成方法,但是不能被抽象为属性。

例如raft为什么需要记录任期term?是因为在选举的过程中,各个节点的状态不是同步的,需要一个任期来判断节点是否滞后,term属性是为了选举这一方法服务的。

也就是说,属性是为了方法服务,方法不需要的属性没必要存在。

  • 有的对象属性比方法重要

比如etcd中的RaftLog,最重要的其实是Entires[]、commitedIndex、appliedIndex这几个变量。而其方法不论有多少if else,本质上都是清一色的get/set方法

函数/方法

函数的本质是什么?是计算,是数据的流转。而数据的呈现形式是变量。因此所有的函数都可以看作是对变量的C(new)、R(data = vector[1])和U(num++),D(delete运算,vector中数据的erase)

再进一步,一个函数中的变量有哪些呢?以OO为例

  1. 对象的成员属性
  2. 函数的传参
  3. 全局变量

此外还有中间变量

因此,在写函数的时候,一是需要先弄清楚这四者,否则不要下手。二是,写完函数之后要想一想,这三者中有没有拉下的create,update(这里不需要考虑Read)

对象的组合

一个用OO写出来的程序,一定是若干对象的继承/组合。每一个对象会有属性,方法,这里的方法可以理解为为上层对象准备的API。若干个对象组合成一个模块(注意:继承不能形成模块,继承只能形成单一的个体),每个模块向外暴露一些API。

因此我们在谈说学写代码不能只能只会调包,要懂程序的原理。而从这个角度看来,程序的原来也无非是一些API与一些对象的属性以及一些重要的实现而已。

所以,我认为API才是整个程序设计的灵魂,明白了各个模块之间的API也就能够弄懂整个程序的架构设计。

具体的落地方法,可以使用CLION/VScode左侧边栏的缩略图,可以从整体看到一个对象

DEMO

如何拆解程序

显然,对于一个庞大的系统来说,要是直接上来扎进源码的实现里,除了浪费时间以及把自己搞得晕头转向以外别无任何其他作用。

因此对于大型的程序,必须要对其进行拆解。

具体落地的措施有

  1. 找资料,找到程序的架构图,它可能是在程序的官网(例如tidb),也有可能在某篇paper中(snowflake),还有可能根本没有官方资料,只有民间大佬所整理的图片(ClickHouse)。根据架构图,理解程序的整体架构,以及有哪些大的模块。

  2. 针对某个模块,看源代码时,先看左侧的API structure结构(vscode中的outline,idea中的structure),理解这个模块究竟做了哪些工作,有哪些功能,这个结构体有哪些属性。由此便可以简单了解这个模块。

  3. 对于raft这种看了接口实现也无法理解其背后原理的程序,先自己yy一个暴力的实现,然后再去看std的实现。实际上这种应用20%的代码就可以实现基本功能,50%在解决corner case, 30%在优化(包括功能扩展与时空优化)