代码的未来

读书 更新时间: 2019-09-08 @ 08:15:32 创建时间: 2019-08-30 @ 08:02:25 浏览数: 282 净访问: 181 By: skyrover

本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循署名-非商业用途-保持一致的创作共用协议


花了几周的时间将松本行弘的另外一本书看完了,同样的感觉:大师的理解真是令人深刻,加上书中语言平易近人,看他的书总有一种春风拂面的感觉,不会觉得那么晦涩难懂。可能由于时间局限性,所以书中也有一些过时的东西,比如现在被诟病的DartCoffeeScript,但是瑕不掩瑜,对于本质的东西,松本行弘大师理解的以及讲出来的还是非常深刻的,令人耳目一新。

编程的时间和空间

编程的本质是思考,正是因为人的可以思考性,编程的工作才是一个创造性的,富有激情的。

摩尔定律引发的计算机变化天翻地覆,但是计算机的本质仍然没有多大变化,以及算法之类的东西仍然是不变的。摩尔定律也出现了局限,物理层面上的,比如漏电流和热密度的问题,都导致了摩尔定律的限制。

对于预测,银河帝国的基地系列给出了心理史学的预测方法,很厉害,哈哈。不过对于IT的预测,作者从4个方面给出了预测:

  • 价格:价格变化不会很大
  • 性能:多CPU,以及可以利用多CPU的编程语言
  • 容量:面向个人的数据仓库之类的数据分析工具,存储速度也会造成巨大变革
  • 贷款:CS系统和中央集权系统的切换

编程语言的过去,现在和未来

编程语言的世界

  • Fortran:Formula Translation 面向科学计算,读书的时候还看过这个代码
  • COBOL:COmmon Bussiness Orientd Language,面向商业的通用语言
  • Lisp:Ruby是借鉴了Lisp,List Processor,
  • SNOBOL:没听说过的语言。。擅长处理字符串,以模板为中心来进行字符串处理
  • 数学性语言:Prolog以一阶谓词为基础,Haskell函数型编程语言
  • 主流语言:C/C++,Ruby,Perl,Python,Java,PHP

编程语言的未来(加上我自己的理解)应该是更加抽象化,人们编程不需要考虑更底层的东西,而是形成抽象化的处理模型之类的,或者更进一步就像作者所说的更强调What,而不是解决问题的How。

另外作者所说的会向分布处理(多计算机协作)方向发展,这样在语言层面上支持这种抽象化,编程就更加方便。

另外编程语言未来将不是独立,而是和各种编辑器,调试器,性能分析工具等共同存在,而且这些工具会变得更加有用和智能,这样才可以提高工作效率。

DSL(领域特定语言)

DSL(Domain Specific Language)特定领域语言,利用为特定领域所专门设计的词汇和语法,简化程序设计过程,提高生产效率。直接使用其特定领域中的概念,集中描述想要做什么,比如makefile来描述依赖关系,和读取它的make命令。

  • 外部DSL

使用专用语言引擎实现的DSL,比如SQL,XML编写的配置文件。外部DSL独立于程序开发的语言。正则表达式也是一种描述字符串模板的外部DSL。这里提到了UNIX编程环境一书中有这些外部DSL迷你语言的制作介绍,有空可以去拜读一下

  • 内部DSL

内部DSL发源于Lisp和Smalltalk文化之中,怪不得Ruby里面经常提到DSL这个东西,内部DSL是在现有语言中实现DSL,作为DSL基础的语言是宿主语言,其中Rails就是作为一种DSL的存在。另外Ruby中的一个例子就是Rakefile,就是为rake命令描述编译规则的DSL代码,而且是一段Ruby程序。

内部DSL语言限制在宿主语言中,自由度低,但是易学易用。

DSL是将面对特定领域的API设计成优秀的DSL,库设计就是语言设计,向语言中添加库,就相当于设计一种规模更大的语言,编程就是为自己应用程序设计DSL的过程。

也就可以理解为添加一个库,这个库就是某个特定领域的DSL,这样结合起来的应用程序就更加抽象和健壮。

所以设计API的时候,可以考虑像设计一种新的DSL一样,这样可能写出的代码会更加优良吧!

元编程

Rails框架的生产性就是通过元编程实现的,描述数据本身的数据->元数据,元编程就是用程序来编写程序,获取和变更程序本身信息的功能成为反射,就像看着镜子反射出的身影来反省查看自己一样,作者的这个比喻简直是令人豁然开朗的理解了反射(reflection)~

后面作者介绍了Lisp,有点晦涩,算了跳过了。。

Ruby元编程中提到一句:根本没有什么元编程,只有编程而已,的确,程序是由数据结构和算法组成,如果环境允许程序本身作为数据结构来操作,那么元编程就和普通编程一样。

内存管理

这里和松本行弘的程序世界里介绍的差不多,不过更细致~

通过垃圾回收机制和操作系统提供的虚拟内存,可以让人感到内存容量是无限的。。但是虚拟内存的来回切换,产生抖动,就会导致电脑死机。

  • 标记清除方式

从根开始将可能被引用的对象用递归的方式进行标记,将没有标记的对象作为垃圾进行回收。该算法的处理时间和存活对象数与对象总数的总和相关。

  • 复制收集方式

将从根开始被引用的对象复制到另外的空间,再将复制的对象能够引用的对象用递归的方式不断复制下去。

这种算法在存活对象比例较高的情况下,反而比较不利,另外的好处是有局部性,因为复制的过程会按照对象被引用的顺序将对象复制到新空间中,所以关系比较近的对象被放在距离比较近的内存空间中可能性会提高。

  • 引用计数方式

在每个对象中保存该对象的引用计数,当引用发生增减的时候对计数进行更新。该算法的最大优点是实现容易,并且在引用计数方式中不再引用的对象是立即释放的,操作也是对每个对象个别执行的,产生的中断时间比较短。

缺点就是无法释放循环引用的对象,第二个缺点就是容易出bug,一旦增减有误,就可能有问题。第三个是不适合并行处理,所以必须独占。

上面三种是最基本的三种GC方式,下面是一些改良的方式:

  • 分代回收

思路就是大部分对象会在短时间内成为垃圾,寿命长的对象更容易存活。所以就对年轻对象重点扫描。

  • 增量回收

与实时性相关,将GC操作细分成多个部分逐一实行,但是可能会有冲突,所以一般会加上写屏障(对所有涉及修改对象内容的地方进行保护),当已经被标记的对象的引用关系发生变化时候,通过写屏障会将新被引用的对象作为扫描的起始点记录下来。

  • 并行回收

在原有程序运行的同时进行GC操作,也需要写屏障来对当前的状态信息保持更新。

  • GC大统一理论

标记清除和复制收集,是从根开始扫描以判断对象生死,称为跟踪回收,而引用计数是当对象之间关系发生变化时候,通过对引用计数进行更新来判定生死的。

任何一种GC算法,都是跟踪回收和引用计数回收两种思路的组合

异常处理

墨菲定律->即便是极少会发生的情况,只要有发生的可能性,早晚是会发生的。

  • 用特殊返回值表示错误

比如C语言,是挺麻烦的一件事情,第一对错误检测不是强制进行,容易崩溃,第二原本的程序容易被错误处理埋没,也就是写太多的判断。。

# include<stdio.h>

int main()
{
    FILE *f = fopen("file", "r");
    if (f == NULL){
        puts("file open failed");
    } else {
        puts("file opened");
    }
}
  • Ruby中的异常处理

和Python中一样,采用了异常的机制,异常会中断正常流程,回溯到当前过程的调用者,然后逐级回溯。但是可以中途捕获异常。

begin
  f = open("file", "r")
  puts("file opened")
rescue
  puts("file open failed")
end
  • Java的检查型异常

Java的这种非得定义的异常类型简直非常让人难受,事先预判什么异常这还需要异常处理吗。。所以看到Java代码大多是定义了一个Exception,这恐怕和当初设计背离了吧!

  • Golang的异常处理

基本上和C语言一样,但是使用的是逗号ok形式,也没有异常处理机制。其实实际写下来也觉得很繁琐,整个代码,判断ok占了多数。

f, ok := os.Open("file", os.O_RDONLY, 0);
if ok != nil {

}
  • 异常和错误值

对hash访问中,Python里key不存在是一个异常,而Ruby是不会产生异常,返回nil的。在Ruby中key不存在的情况是在某种程度的设计返回内,所以返回nil,而Python中是作为超出设计返回,所以用异常来处理。Ruby中仍然有fetch方法,在key不存在的时候产生异常。

闭包

闭包这个概念在我们公司面试中是必问的一项,但是很少有面试者能答对,很多人仅限于知道Python中的装饰器是闭包实现的,而并不知道闭包到底是什么意思。这里作者给了很透彻的解释。

首先函数对象并不是闭包,在C语言中函数对象是函数指针,在其他的一些编程语言中,函数对象类似一个值,可以拿来操作。

#include <stdio.h>
int two(int x) { return x * 2;}
int three(int x) { return x * 3;}

int main(int argc, char **argv){
    // 接收int参数,返回int值的函数指针
    int (*times)(int);
    int n = 2;

    if (argc == 1) times = two;
    else times = three;
    printf("times(%d) = %d\n", n, times(n));
}

函数对象最大的用途是高阶函数,也就是把函数作为参数的函数,比如C语言标准库中定义的通用排序函数qsort。高阶函数的这种方式,通过将一部分处理以函数对象的形式转移到外部,实现了算法通用化。

// 第四个参数是指向一个带两个参数的函数的指针。
void qsort(void *base, size_t nmemb, size_t size, int(*compar)(const void *, const void *));

int icmp(const void *a, const void *b){
    int x = *(int *)a;
    int y = *(int *)b;

    if (x == y) return 0;
    if (x > y) return -1;
    return 1;
}

虽然函数指针很强大,但是也有弱点,而对外部局部变量的访问是C语言函数指针的最大弱点,总不能因此来定义一个副作用很大的全局变量吧?因此为了解决这个问题,所以出现了闭包。

两个关键术语:作用域和生存周期

  • 作用域

指的是变量的有效范围,比如下面的js程序,foreach的参数是一个函数对象,但是可以访问在外部声明的变量i,因此C语言中无法做到的事情,这里就可以做到。

function foreach(list, func){
    while(list){
        func(list.val);
        list = list.next;
    }
}
 var list = null;
for(var i = 0; i < 4; i++){
    list = {val: i, next: list};
}
var i = 0;
foreach(list, function(n) {console.log("node(" + i + ") = " + n); i++;})

所以从函数对象中能够对外部变量进行访问(引用,更新),是闭包的构成要件之一。

  • 生存周期

比如下面的程序:

function extent(){
    var n = 0;
    return function (){
        n++;
        console.log("n=" + n);
    }
}
f = extent();
f(); // 1
f(); // 2

从上面可以看到从属于外部作用域中的局部变量,被函数对象给封闭在里面了,其寿命与封闭它的函数对象寿命相等,也就是f,当f不再被访问,被回收掉,这个局部变量的寿命就终结了。

所以在函数对象中,将局部变量这一环境封闭起来的结构称为闭包。

之前在写Python的装饰器的时候,就觉得闭包这东西其实就在某种意义等同于类,同样有其内部数据(局部变量),也有其处理方法(内部函数),同时不会污染上层变量。这就意味着函数与数据结合起来了,而类不就是做这个事情的吗?

过程与数据的结合是形容面向对象中的对象,对象是在数据中以方法的形式内含了过程,而闭包是在过程中以环境的形式内含了数据,精彩!

  • Ruby的函数对象

因为Ruby是纯粹面向对象的语言,所以没有函数的概念,Ruby中的一切过程都是从属于对象的方法,但是Ruby中有具备和函数对象相同功能的Proc对象,所以Ruby中的闭包是这样写的:

def extent
  n = 0
  lambda {
      n += 1
      printf "n = %d\n", n
  }
end
f = extent();
f.call();
f.call();

编程语言的新潮流

这一部分其实作者只是走马观花的介绍了不同语言以及其相同和不同之处,总的来说没有什么让我觉得印象深刻的干货,所以也就不详细介绍了。

总体上,我觉得Golang这门语言是非常有前景的,之前也用Golang写过一些服务器端代码。有下面几个感受:

  1. 首先语法上和C语言很像,让我感觉是高级的C语言
  2. 没有异常处理,一开始不是很习惯,后面习惯了每次检查ok觉得很麻烦
  3. goroutine是一个创造性的东西,没了一般编程语言的进程线程协程控制
  4. 无继承式的面向对象,一开始一头雾水,后来慢慢习惯了
  5. GoPATH之类的东西,也就是包管理上,感觉麻烦

这是我一些浅显的感受,自己也没有深入研究Golang,所以仅限于此,后面有机会再继续研究。

云计算时代的编程

可扩展性

在IT世界中也发生了和物理学一样的问题,从小尺度上,电路的精密化导致量子力学的影响开始显现,从而影响到摩尔定律,从大尺度上说,则产生了信息爆炸大致的海量数据问题。

  • 二分查找

Python实现

class Solution:
    def bsearch(self, A, value):
        """
        :type A: List[int]
        :rtype: int
        """
        lo = 0
        hi = len(A) - 1
        while lo <= hi:
            mid = lo + (hi - lo) // 2
            if A[mid] == value:
                return mid
            elif A[mid] < value:
                lo = mid + 1
            else:
                hi = mid - 1

Ruby的实现

def bsearch(data, value)
  lo = 0
  hi = data.size
  while lo < hi
    mid = lo + (hi - lo) / 2
    if data[mid] == value
      return mid
    elsif data[mid] < value
      lo = mid + 1
    else
      hi = mid
    end
end
  • 散列表

散列表是表达一个对象到另一个对象的映射的数据结构,是一种采用非数值型索引的数组,所以也被称为联想数组。

散列表中需要一个散列函数,用于将各个值计算称为一个称为散列值的整数,需要满足

  1. 从数据到整数值的映射
  2. 足够分散
  3. 不易冲突

但是会出现两个问题,散列值冲突和数组溢出,应对冲突有两种方法:

  1. 链地址法:将相同散列值的元素放在链表中,随着元素个数增加,散列冲突和查询链表的时间也随着增加,Ruby使用这种
  2. 开放地址法:开放地址法是遇到冲突的时候,再继续寻找一个新的数据存放空间,最简单的方法是按顺序遍历,实际上会更复杂点,Python使用这种
  • 布隆过滤器

判断某个数据是否存在,即判断集合中是否包含某个成员,其判断时间和数据个数无关,空间效率也非常好,但是无法删除元素,偶尔会出错。毕竟它是一个概率算法。

它使用k个散列函数和m个比特的比特数组,向布隆过滤器插入数据的时候,会对该数据求得k个散列值,并以每个散列值为索引,将对应的比特数据中的比特位全部置为1。所以要判断是否包含某个数据,需要求得数据的k个散列值,只要其对应比特位有任意一个为0,则可以以判断集合中不包含该数据。

  • 分布式散列表(Distributed Hash Table,DHT)

会包含以下算法:CAN, Chord, PAstry, Tapestry等。P2P环境下可能出现下列问题:

  1. 由于机器故障灯原因导致节点消失
  2. 节点的复原,添加
  3. 伴随上述问题产生的数据位置查找(路由)困难的问题

后面作者讲了自己工作开发的Roma键值存储数据库,其采用了环状的分布式架构。

C10K问题

Client 10000 Problem,即使硬件性能足够,依然无法正常提供服务。利用keep-alive实现的高实时性推送服务,会导致更多的并发连接对服务器造成载荷,所以会更快遇到C10K问题。现在恐怕已经需要应对C1000K问题了吧。。不过无论如何本质其实一样的。

最弱连接:在构成服务的要素中,哪怕只有一个要素没有考虑到超过一万个客户端你的情况,这个要素就会成为最弱连接。

如果进程数随着并发连接数等比例增加的话,是无法处理大量的并发连接的,这时候就需要像事件驱动模型等软件架构层面的优化了。

另外文件描述符也有上限,文件描述符过多,使用select系统调用来监视它们是否处于可供读写的状态时候就有问题,因为每次都需要把监视中的文件描述符全部检查一遍。可选的方案有epoll, kqueue等用于监视文件描述符的API,或者是非阻塞IO。

之前有写过一篇专门介绍IO模型的文章,可以参考:IO模型

在Ruby中可以使用EventMachine这种事件驱动框架来对付C10K问题,这里就不详细看了,以后有需要再详细研究。

HashFold

HashFold是MapReduce的一种变体,MapReduce通过分解,提取数据流的Map函数和化简,计算数据的Reduce函数,对处理进行分割,从而实现了对大量数据的高效分布式处理。而HashFold是以散列表的方式接收Map后的数据,然后通过Fold过程来实现对散列表元素的去重复。

下面是书中的一个Ruby实现HashFold单词计数程序:

class HashFold
  def start(inputs)
    hash = {}
    inputs.each do |input|
      self.map(input) do |k, v|
        if hash.key?(k)
          hash[k] = self.fold(hash[k], v)
        else
          hash[k] = v
        end
      end
    end
    hash
  end
end

class WordCount < HashFold
  STOP_WORDS = %w(a an and)  # 其他省略了
  def map(document)
    open(document) do |f|
      for line in f
        line.gsub!(/[!#"$%&\'()*]/, " ")
        for word in line.split
          word.downcase!
          next if STOP_WORDS.include?(word)
          yield word.strip, 1
        end
      end
    end
  end

  def fold(count1, count2)
    return count1 + count2
  end
end

Ruby中线程功能是在解释器级别上实现的,为了保护解释器中非线程安全的部分而加上了一个GIL锁,所以每次只能同时执行一个线程。

fork和pipe在使用进程的程序中几乎是不可或缺的技巧。fork的作用是创建一个当前运行中的进程副本,所以子进程可以引用类和对象,但是如果有变更,是无法反映到父进程中。

pip方法会创建两个分别用来读取和写入的IO,写入到w的数据可以从r中读取出来,所以在fork之后,将父进程的w关掉,将子进程的r关掉,这样从子进程中写入,就可以在父进程中读取了。

class HashFold
  def hash_merge(hash, k, v)
    if hash.key?(k)
      hash[k] = self.fold(hash[k], v)
    else
      hash[k] = v
    end
  end

  def start(inputs)
    hash = nil
    inputs.map do |input|
      p, c = IO.pipe
      fork do
        p.close
        h = {}
        self.map(input) do |k, v|
          hash_merge(h, k, v)
        end
        Marshal.dump(h, c)
      end
      c.close
      p.each do |f|
        h = Marshal.load(f)
        if hash
          h.each do |k, v|
            hash_merge(hash, k, v)
          end
        else
          hash = h
        end
      end
    end
    hash
  end
end

但是这样会导致一个问题:大量创建进程导致抖动(巨大的内存开销),所以可以不每次都创建进程然后舍弃,可以重复利用已经创建的进程,这种技术称作池(pooling)

进程间通信

同一台计算机上进行进程间通信的手段有以下几种:

  1. 管道:系统通过pipe调用创建了一对文件描述符进行通信
  2. 消息:数据通信
  3. 信号量:互斥锁
  4. 共享内存:进程间共享内存状态
  5. TCP套接字:写入的数据作为单纯的字节流
  6. UDP套接字:写入的数据是一个包,可以获取写入的数据长度
  7. UNIX域套接字:使用和文件一样的路径来指定连接目标,仅用来在同一台计算机上通信,还可以传输文件描述符

利用套接字,可以和另外的计算机通信,但是套接字只能传输字节序列,要传输文本之外的数据,需要转换为字节序列,这种转换一般称为序列化(serialization)或者封送(marshaling),序列化一般是左右整体性能的重要因素

Rack 与 Unicorn

web应用服务器主要由HTTP服务器与Web应用程序框架构成,HTTP服务器比如Apache, nginx, Ruby中的WEBrick,以及Mongrel和Thin

Ruby中的Web服务框架除了RoR之外,还有Ramaze, Sinatra等。

Rack是在Python的WSGI影响下开发的用于连接HTTP服务器与框架的库。HTTP服务器一端,只需要对Rack发送请求,然后接受响应并且进行处理,就可以连接所有支持Rack的框架,框架这边只要提供对Rack的支持,就可以支持大多数HTTP服务器。

Rack基本原理,是对Rack对象发送HTTP请求环境作为参数调用call方法,并将以返回值方式接收的请求组织成HTTP请求。

  • Rack中间件

因为调用规则比较简单,所以在实际中对框架进行调用之前补充通用功能的call,就可以在不修改框架前提下,对所有支持Rack的web应用程序增加通用性功能。

当网站流量达到一定规模,常见做法是将Apache和Nginx放在前面进行负载均衡,实际的应用程序则通过Thin等其他HTTP服务器进行工作。这种模式属于推模式,但是并不知道目标服务器的状态,所以很容易导致下面的服务器分配不均。

  • Unicorn

在使用Unicorn的架构中,Apache或者nginx负责和Unicorn中的Master进程进行通信,一般使用Unix域套接字完成,如果是分布式环境,那么使用TCP套接字,转发给Unicorn-Master的请求,会转发到由Master通过fork启动的Slave,等待Slave处理完成之后,将响应转发给Apache或者Nginx

Unicorn这种机制属于拉模式,所以对于Slave来说,做完任务再去拉,就不存在分配不均的情况了。另外一大优点就是可以快速重启。

缺点是Unicorn只适合每个请求处理时间很短的应用,另外一个项目Rainbows!可以对N个进程分配M个请求,从而缓和大量的连接数和有限的进程数之间的落差。

Python里面有个Gunicorn,这两个是谁参考谁啊。。

Gunicorn 'Green Unicorn' is a Python WSGI HTTP Server for UNIX. It's a pre-fork worker model ported from Ruby's Unicorn project. The Gunicorn server is broadly compatible with various web frameworks, simply implemented, light on server resource usage, and fairly speedy.

支撑大数据的数据存储技术

键-值存储

  • 数据库的ACID特性
  1. Atomicity(原子性)
  2. Consistency(一致性) 数据库状态必须永远满足给定的条件
  3. Isolation(隔离性) 保持原子性的一系列操作的中间状态,不能由其他事物进行干涉
  4. Durability(持久性) 原子性一系列操作完成时候,结果会被保存而不会丢失
  • CAP原理

ACID无法扩展,CAP原理:

  1. Consistency(一致性)
  2. Availablity(可用性)
  3. Partition Tolerance(分裂可容忍性)

上面三个性质,只能同时满足两个

  • CAP解决方案-BASE

分布式系统中,局部故障会导致整体故障的要害,被称为单一故障点,分布式系统中是需要极力避免的。所以C是没法舍弃的。对于A当然不能舍弃。

所以只有C,现实中严密的一致性是几乎不存在的,所以BASE这样的思路更合适:

  1. Basically Available 重视可用性
  2. Soft-state 不追求状态严密性
  3. Eventually consistent 最终一致

ACID酸和BASE碱相对,是个巧合吗?

NoSQL

在大规模环境下使用关系型数据库,一般有水平分割和垂直分割两种方式

  • 水平分割:将一张表的各行数据直接分割到多个表中,比如奇数在这个表,偶数在那个表等
  • 垂直分割:将一张表中的某些字段分离到其他表中

  • ActiveRecord

ActiveRecord将表中各条记录作为对象来操作,也是一种ORM。数据库结构信息和模型定义是分离的,但是不会对数据库结构定义进行重复描述,数据库结构信息只存在于它原本存在的地方,也就是数据库中,这一点上比Python的SQLAlchemy要好用点,Python中改了数据库定义,还得改模型定义,一点都不符合DRY原则,有时候就会忘记改模型定义,导致bug。

信息的重复往往是造成bug的元凶,对数据的操作和对象之间的关系,在模型中定义就可以了,对象结构信息只在数据库中存储,模型定义的就是对象的关系信息。


点赞走一波😏


评论

提交评论