RSS Feed

April, 2012

  1. JS的面向对象

    April 27, 2012 by xudifsd

    JS在很长一段时间内都被认为是没什么多大用处的语言,而且JS的程序员也不被重视,常常有类似于这样的程序员鄙视图,而且JS程序员总是处在被很多其他程序员鄙视的地位。但是原因主要是由于JS只能在浏览器中使用,出了浏览器JS什么也干不了。以前学《JavaScript语言精粹》看它介绍了很多复杂的面向对象的东西,我当时就觉得奇怪,为什么一个网页上的程序要这么复杂的技术,所以当时很多东西都直接跳过。但是现在有了Node,它将JS从浏览器中解放了出来,并且用在了高性能的服务器端,这让JS有了很大的用途。所以重新复习了下《JavaScript语言精粹》,记一下笔记。

    JS的面向对象

    JS支持面向对象,只是它的支持方法有些非主流,它使用“原型链”来实现面向对象。只是原型链对于传统编程人员来说很难理解,但是一旦理解其实会发现这种轻量级的继承表现力很强。

    JS有很好的对象的字面量表示,也正是因为JS的这种强大表示方法才有了JSON这种通用的数据交换格式。所以在JS中可以直接写出在其他语言需要“实例化”才能产生的对象。但是实际编程时,往往需要“未实例化”的对象的模板,也就是“类”,这样才能创造出许多相似但不同的“对象”。显然用JS对象的字面量来创造对象不能实现这一点,为了支持这一点,JS支持一种“伪类”,也就是原型链。

    用下面的代码就可以创造出一个叫“Cat”的伪类,和一个叫“cat”的实例,且cat有个实例变量name。

    var Cat = function(name) {
    	this.name = name;
    };
    
    var cat = new Cat("foo");

    这个语法和Java很像,但是这个例子并没有用到原型链,所以也就没有实现继承的。

    下面的代码用原型链实现的继承:

    var Animal = function(sound) {
    	this.sound = sound;
    };
    
    var Cat = function(name) {
    	this.name = name;
    };
    
    Cat.prototype = new Animal("meow");	//实现继承
    Cat.prototype.says = function() {
    	return this.sound + ', ' + this.name;
    };

    这里Cat伪类继承了Animal,通过原型链Cat子类可以访问在父类中定义的变量sound(通过this.sound访问)。但是这一段程序估计那些用正经类的人根本看不懂。下面进行一些解释:

    JS中的函数有四种调用方法

    一般静态语言的程序调用函数非常简单,将参数传入,取得返回值就行。支持闭包的程序就更复杂一些了,由于函数可以动态定义,所以还需要考虑函数定义时的环境。JS也支持闭包,但是JS的函数调用更加复杂:不同的函数调用方法会影响this关键字指向什么东西。

    JS的函数总共有四种调用方式:

    1. 方法调用模式
    2. 函数调用模式
    3. 构造器调用模式
    4. Apply调用模式

    这里只说构造器的调用模式。其他的可以看《JavaScript语言精粹》的4.3节。

    面向对象的特点之一就是允许对象有自己的私有空间,对象外部必须通过对象才能引用对象的私有空间内容,而对象的方法也需要有个关键字来引用自己的私有空间,在JS中这个关键字就是this。前面说的四种调用方法就会影响this的绑定。

    并且JS的函数的this是超级迟的绑定,声明了函数之后this是不一定绑定在哪,这样就允许JS中对函数的this进行高度地复用。在函数的构造器调用模式下this是绑定到一个新创建的对象,并且这个对象有一个隐藏连接到函数的prototype对象(注意,函数也是对象,它也有自己的属性)。构造器调用模式最后会返回这个新创建的对象。

    图解

    一图胜千言:

    var father = {
    	name: 'father'
    };

    这里用JS对象的字面量直接声明一个对象,这时内存中的样子像这样:

    var Child = function(name) {
    	this.name = name;
    };

    这里声明一个函数,由于JS支持闭包,所以这时在内存中函数还会有闭包属性,这个闭包包括了定义Child函数的环境的所有信息(在调用函数时会用到),但与其他语言不同的时JS的函数对象还有prototype属性,由于JS无法知道哪些函数是作为构造器来调用的,所以每个函数都会有这个属性,它指向一个有constructor属性的对象,并且该constructor指向函数本身:

    Child.prototype = father;

    这里修改了Child的prototype属性,令它指向前面创建的father对象,这时如果修改了father.name属性,那么也会在Child.prototype.name中体现:

    Child.prototype.get_name = function() {
    	return this.name;
    };

    给Child.prototype添加一个get_name属性,这个属性指向一个新创建的函数,该函数也有自己prototype和闭包属性,并且由于Child.prototype和father指向同一个对象,father也就有了get_name属性:

    var mychild = new Child('foo');

    这里是整个原型链的关键,注意必须用new来调用Child函数,这样JS才知道Child是以构造器的方式来调用的,否则如果没有new那么JS就会以普通函数来调用,这样this就会错误地绑定到全局变量上去,造成全局变量中name被修改或者定义,而且mychild会变成undefined,这样会造成很难发现的bug。另一个值得注意的地方是mychild从此有了一个从Child.prototype继承下来的隐藏的prototype属性,这个属性无法通过mychild直接访问到,只是在访问未在mychild中定义而在father中定义的属性会用到(在这个例子中如果访问mychild.get_name也会得到想要的函数,并且由于get_name的this没有绑定到任何地方,所以如果用father.get_name调用,this会绑定father,如果用mychild.get_name调用则会绑定到mychild上)。但是如果访问mychild.name时会直接得到’foo’,因为JS读任何对象的属性都遵守这样的一个规则:如果对象本身没有该属性,就到对象的隐藏的原型链中去找,如果再没有再找对象原型的原型,直到搜到原型链底部或者找到。但是写或者删除对象的属性就不会涉及到原型链。

    delete mychild.name;

    在里用delete语句将mychild的name属性删除,所以mychild没有了name属性,但是这种删除会让原型链中的内容暴露出来,这时如果访问mychild.name会得到’father’,避免这种情况的方法就是通过mychild.hasOwnProperty(‘name’)的返回值来确定mychild本身是否有该属性。如果执行了删除后再执行delete mychild.name并不会删除father.name,因为删除或者写属性不会涉及到原型链:

    短处

    这种伪类的继承方法本身很好,但是由于必须用new来调用函数,容易造成一些难以发现的bug,所以《JavaScript语言精粹》的作者并不推荐这种方法,他主张用函数的闭包来实现继承,闭包可以到这里看,虽然是用scheme语言做例子,但是原理都一样,在这里就不废话了。


  2. pthread学习总结

    April 5, 2012 by xudifsd

    线程的坏处

    在理解线程和进程之前我就已经看了《Unix编程艺术》,并且被书的第7章吓得不浅。这一章大谈线程如何如何恶心,进程如何如何优秀云云。现摘抄几句:

    一个操作系统,如果没有灵活的IPC(进程间通讯)和使用IPC的强大传统,程序间就得通过共享结构复杂的数据实现通讯。由于一旦有新的程序加入通讯圈,圈子里的所有程序的通讯问题都必须重新解决,所以解决方案的复杂度与协作程序数量的平方成正比。更糟糕的是,其中任何一个程序的数据结构发生变化,都说不定会给其他程序带来什么隐藏的bug。

    线程成为滋生bug温床源于它们太容易知道过多彼此的内部状态。与有着独立地址空间、必须通过明确IPC进行通信的进程不同,线程没有自我封装。这样,基于线程的程序不仅产生普通的竞争问题,而且产生了新一类bug:时序依赖,要重现这些问题都极其困难,遑论修复。

    因此在学习《The Linux Programming Interface》时特意把几乎所有重要的东西都学完才开始学线程。并且要不是想要学下并发估计这辈子都不可能接触到线程了。

    整体来说,线程这个概念是在Unix存在了很久之后才引进的,所以很多Unix的概念都与线程不兼容,比如说信号。因此,如果要在程序中使用信号,那就不要使用线程;如果要使用线程就不要使用信号。否则程序会变得异常复杂。而且由于上面提到的原因,在传统的Unix平台上编程还是要避免使用线程。

    线程的好处

    线程这种技术既然存在就一定有它的好处,Raymond说它是为了解决一些进程生成代价高昂的系统的问题而产生的,有一定道理。但是POSIX都定义了它,并且今年GSOC的git项目都有Improving parallelism in various commands这样的idea,那么说明线程还是有市场的。但是这些理由都不是很充分。

    我觉得对于学习,最重要的不是知识而是思想。学习线程就能学到一些很重要的并发思想:如果你没有学过线程可能连reentrancy, thread-safy这些概念完全不懂,这是因为在进程中这些概念根本不重要,但是一旦进入线程的世界这些概念变得至关重要,否则你的程序就会出现前面Raymond提到的时序依赖bug。

    总的来说,编写传统的进程程序完全可以不在乎静态变量,全局变量,堆变量在不同函数之间如何同步,因为一个进程实体中永远只有一个函数在执行,并且这些资源在进程间都不是共享的(进程间唯一共享的资源恐怕就是文件系统了)。所以传统程序更容易编写,也更不容易出错。

    但是一旦到了多线程,上面的断言就错了:总是有多个函数在一个进程实体中执行(否则也就没有分多个线程的必要了)。所以线程需要一些诸如mutex,condition variable之类的同步方法来避免时序依赖。

    因此,写多线程程序就像执行一个非常精细的手术,如果连多线程程序都能正确编写那么写个多进程的程序简直就是小菜。并且一些其他的技术,诸如:原子操作,死锁,临界区域等也就非常容易理解了,这对于写一个并发的大系统很有好处。

    不同语言并发的实现

    Unix的传统是使用进程,而C算是Unix的附带品,所以C一般是使用fork()来实现并发,而C语言中使用线程就比较复杂,为了避免时序依赖还需要很多mutex和condition variable。但是看了python中的并发实现后就震惊了:程序有个全局的类实例queue,但是在访问这个类实例时竟然不需要使用任何mutex!看来python本来就已经把Queue设计成多线程中可以方便访问的了(这里也可以得出一个结论:要想深入理解一些内容最好从一些较低级的语言开始,如果一开始就用高级语言,而高级语言又把很多复杂的内容封装起来,那么根本就学不到什么东西),在高级语言中一般主要还是使用线程实现并发,甚至在Java中我都没发现有fork这个东西。JS的并发就更好实现了,ajax和事件驱动直接搞定一切,所以以这个为设计思想的Node可以非常轻松的实现并发。

    延伸

    从上面的一些总结可以知道:并发有线程和进程两种实现方法。但是这些方法都是最为底层的内容,编写大型系统时用到的会比较少。而现在使用的最为普遍的模型就是Google的MapReduce了,这个模型的最大好处就是并不要求使用者有很多的并发编程的经验,所有的任务分配,容错都是在系统内部解决。真正的MapReduce不是开源的,但是已经有了开源的MapReduce实现,那就是Hadoop。以后有空了应该把Hadoop学学。

    现在也只是掌握了最为基本的线程,熟悉了pthread的API而已,要深入还需要很多的功夫啊。