主题:【原创】介绍一下Go语言(1)之前的话 -- zllwy
加到statement里面:-D
netty和mina不是同一个project,netty是redhat/jboss的,mina是apache的。不过干的倒是同样的事情。
好在goroutine有Go的runtime自己进行scheduling,程序员不用管。
coroutine如果让程序员自己管理,规模一大就很难控制,就像netty的异步编程方式。所以erlang,go在语言层面解决这个问题,减轻了程序员的负担,也使代码复杂度得到了控制。
生生废了编辑器的自动排版功能,人为地把copy-paste变成艰巨的任务。
多一对{}难道会死吗?
后来有一段时间netty不怎么更新了,主要作者一个韩国人推出了一个类似的开源项目mina,我就改行用mina了。一度以为netty被废弃了。刚才查了一下,netty还继续存在,所以我说netty改名叫mina是错的,呵呵。多谢指正。
我是09年进入erlang领域的,erlang可说的太多了,一下不知道说啥,以后找机会慢慢说,呵呵。
Go的一大特点是它的type system。在写这篇之前我发现自己其实对很多概念也一知半解,这里就先把type system的一些概念梳理一下。如果有谬误,希望给我指出。
Go有很多方面和python很像。而python又是一个比较有代表性的语言,我这里就主要用Go和python做比较。
首先是static type和dynamic type。static type简单说就是type checking是在compile time做的,而dynamic type是在runtime做的。Go是statically typed而python是dynamically typed。说一下我对这两者的评价。我是基本倾向于用static type语言的,特别是大型项目。支持dynamic type的人的最主要理由就是实际当中type error是很少的(我的体会是其实很常见,而且很难调试),所以到runtime去检查没什么,但开发要快很多。其实我不是很赞同。因为我觉得特别对于大型系统来说,coding其实不占主要时间,design和debugging更花时间。而且static type语言一般效率要高一些。综合来看,dynamic语言除了用来写script,tool比较好以外,真正重要的项目还是应该用static语言。
然后是weakly typed和strongly typed。strongly typed是指任何使用value的时候都要检查它的type,对不上的话就不行。当然这个可以在runtime做(对dynamic type语言)或者在compile time做(对static type语言)。Go和python都是strongly typed。Javascript就不是。这里对python可能有些容易混淆的地方。python可以写:
onevar = 1
onevar = "1"
这个不说明python是weakly typed。因为赋值给变量其实只是一个绑定,关键还是看在用它的值的时候是不是检查type。
还有一个概念是type safety。你不能说一个语言是type safe还是不safe,你只能说多大程度上是safe的。比如C是某种程度上的safe,还是有很多漏洞。C++强一些。Java,C#更强。所以static type system不一定就意味着很强的type safety。
再来讲polymophism,多态。C++用type hierarchy来实现多态。C++的type system比较复杂,有单继承还有多重继承。Java就简化了一下。Java的Interface提供了一定的灵活性。你不必一定要继承某个类,实现一个interface就可以实现多态了。实际上有人建议尽量使用interface而少用inheritance。这个后面再说。但interface的问题是如果有些第三方的代码你不能改,你就很难把它融入到你的type system里面来。这就出现了duck typing和structural typing。duck typing和structural typing很类似。都是说只要你实现了某些性质,你就可以被称为某个类。有人称为signature based polymorphism。duck typing的代表就是python,很有名的一句话就是:
"When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck."
所以duck typing是看一个value是否存在某组属性来进行type checking的。structural typing也很类似,如果一个类实现了一组确定的接口,它就可以被当作那套接口定义的type。区别在于,duck typing是在runtime检查,而strcutural typing在compile time检查。Go是structurally typed。
再来说一下inheritance和composition。如果你要subclass一个基类,你可以选择用"as-a"还是"has-a"。"as-a"就是继承,说明新的类是基类的一种。"has-a"就是组合,说明新的类拥有基类的能力。目前的主流看法是尽量用"has-a",也就是composition。这里就不展开了。Go提供了一些方便使得composition可以基本取代inheritance。这样Go的type system就非常简单了。
现在可以来说Go的type system了。首先Go里面只有struct,没有class。但你可以在任何type上定义函数,设置在primitive type上定义。比如:
type INT int
func (a INT) printa() {
fmt.Println(a)
}
这里你不能直接在int上定义函数,要先定义自己的type,然后在上面定义函数。然后:
var a INT = 1
a.printa()
你实际上就是在整数类型上定义了一个新的操作。结合struct,你可以给任何一个struct定义函数。基本上,你可以把被定义的struct看做是那个函数的一个参数。这样我们就有类似class的功能了。
如果你又定义了一个指针:
var ap *INT = &a
ap就也有了a的函数。你可以写
ap.printa()
更进一步,你可以组合struct来实现subclassing。比如
type T1 struct {
a int
}
type T2 struct {
T1 // anonymous field
b, c int
}
现在T2有了T1所有的东西,你可以直接写:
var t T2
t.a
t.b
t.c
T2还可以直接调用T1的函数。
再来看Go是怎么定义两个类型的可赋值性的(assignability)。如果你要把一个值赋给一个lhs,Go值看两边的类型是否完全等价。比如:
type T1 struct {
a, b int
}
type T2 struct {
a, b int
}
在C/C++里面,这是两个类,是不能赋值的。但Go把他们当作是一样的。
最后,终于说到interface了。如果你有这么一个interface:
type Api interface {
Foo(a int) int
Bar(b float) float
}
这样只要任何一个type实现了这两个函数,就可以被当作Api来用。这样就实现了polymorphism,而且比用type hierarchy的要灵活。你可以比较容易地对你的代码进行改进,而不需要担心要对已有代码做大手术。
以前有段时间想给西西河弄一个 jabber server,基于XMPP的开源标准,类似MSN这样的即时聊天功能。(现在的google talk似乎也是基于xmpp)。找着找着,就碰到一个用 erlang 写的服务端。刚才查了一下,还在 http://www.ejabberd.im/。
看上去挺有趣的。不过最后还是没弄。
看了一下大家的回复,好像讨厌的居多,不过俺也不能因为少数派就说违心的话啊。
缩进的好处,慢慢就有体会了。如果体会不出来,也就算了,呵呵。
先从著名的c10k问题谈起。有一个叫Dan Kegel的人在网上(http://www.kegel.com/c10k.html)提出:现在的硬件应该能够让一台机器支持10000个并发的client。然后他讨论了用不同的方式实现大规模并发服务的技术,归纳起来就是两种方式:一个client一个thread,用blocking I/O;多个clients一个thread,用nonblocking I/O或者asynchronous I/O。目前asynchronous I/O的支持在Linux上还不是很好,所以一般都是用nonblocking I/O。大多数的实现都是用epoll()的edge triggering(传统的select()有很大的性能问题)。这就引出了thread和event之争,因为前者就是完全用线程来处理并发,后者是用事件驱动来处理并发。当然实际的系统当中往往是混合系统:用事件驱动来处理网络时间,而用线程来处理事务。由于目前操作系统(尤其是Linux)和程序语言的限制(Java/C/C++等),线程无法实现大规模的并发事务。一般的机器,要保证性能的话,线程数量基本要限制几百(Linux上的线程有个特点,就是达到一定数量以后,会导致系统性能指数下降,参看SEDA的论文)。所以现在很多高性能web server都是使用事件驱动机制,比如nginx,Tornado,node.js等等。事件驱动几乎成了高并发的同义词,一时间红的不得了。
其实线程和事件,或者说同步和异步之争早就在学术领域争了几十年了。1978年有人为了平息争论,写了论文证明了用线性的process(线程的模式)和消息传递(事件的模式)是等价的,而且如果实现合适,两者应该有同等性能。当然这是理论上的。针对事件驱动的流行,2003年加大伯克利发表了一篇论文叫“Why events are a bad idea (for high-concurrency servers)”,指出其实事件驱动并没有在功能上有比线程有什么优越之处,但编程要麻烦很多,而且特别容易出错。线程的问题,无非是目前的实现的原因。一个是线程占的资源太大,一创建就分配几个MB的stack,一般的机器能支持的线程大受限制。针对这点,可以用自动扩展的stack,创建的先少分点,然后动态增加。第二个是线程的切换负担太大,Linux中实际上process和thread是一回事,区别就在于是否共享地址空间。解决这个问题的办法是用轻量级的线程实现,通过合作式的办法来实现共享系统的线程。这样一个是切换的花费很少,另外一个可以维护比较小的stack。他们用coroutine和nonblocking I/O(用的是poll()+thread pool)实现了一个原型系统,证明了性能并不比事件驱动差。
那是不是说明线程只要实现的好就行了呢。也不完全对。2006年还是加大伯克利,发表了一篇论文叫“The problem with threads”。线程也不行。原因是这样的。目前的程序的模型基本上是基于顺序执行。顺序执行是确定性的,容易保证正确性。而人的思维方式也往往是单线程的。线程的模式是强行在单线程,顺序执行的基础上加入了并发和不确定性。这样程序的正确性就很难保证。线程之间的同步是通过共享内存来实现的,你很难来对并发线程和共享内存来建立数学模型,其中有很大的不确定性,而不确定性是编程的巨大敌人。作者以他们的一个项目中的经验来说明,保证多线程的程序的正确性,几乎是不可能的事情。首先,很多很简单的模式,在多线程的情况下,要保证正确性,需要注意很多非常微妙的细节,否则就会导致deadlock或者race condition。其次,由于人的思维的限制,即使你采取各种消除不确定的办法,比如monitor,transactional memory,还有promise/future,等等机制,还是很难保证面面俱到。以作者的项目为例,他们有计算机科学的专家,有最聪明的研究生,采用了整套软件工程的流程:design review, code review, regression tests, automated code coverage metrics,认为已经消除了大多数问题,不过还是在系统运行4年以后,出现了一个deadlock。作者说,很多多线程的程序实际上存在并发错误,只不过由于硬件的并行度不够,往往不显示出来。随着硬件的并行度越来越高,很多原来运行完好的程序,很可能会发生问题。我自己的体会也是,程序NPE,core dump都不怕,最怕的就是race condition和deadlock,因为这些都是不确定的(non-deterministic),往往很难重现。
那既然线程+共享内存不行,什么样的模型可以帮我们解决并发计算的问题呢。研究领域已经发展了一些模型,目前越来越多地开始被新的程序语言采用。最主要的一个就是Actor模型。它的主要思想就是用一些并发的实体,称为actor,他们之间的通过发送消息来同步。所谓“Don’t communicate by sharing memory, share memory by communicating”。Actor模型和线程的共享内存机制是等价的。实际上,Actor模型一般通过底层的thread/lock/buffer 等机制来实现,是高层的机制。Actor模型是数学上的模型,有理论的支持。另一个类似的数学模型是CSP(communicating sequential process)。早期的实现这些理论的语言最著名的就是erlang和occam。尤其是erlang,所谓的Ericsson Language,目的就是实现大规模的并发程序,用于电信系统。Erlang后来成为比较流行的语言。
说到这里,比较理想的并发机制已经成型了。我们需要轻量级的并发实体(动态堆栈,用户空间的合作调度),需要类似Actor/CSP的消息传递机制。Go就是提供了这样的功能。Go的并发实体叫做goroutine,类似coroutine,但不需要自己调度。Runtime自己就会把goroutine调度到系统的线程上去运行,多个goroutine共享一个线程。如果有一个要阻塞,系统就会自动把其他的goroutine调度到其他的线程上去。Goroutine的堆栈最开始只有几十KB,可以动态在heap上增长。所以程序可以创建几千几万个goroutine。Goroutine的创建很简单:
go foo()
foo可以是一个函数调用,也可以是个closure。Goroutine通过channel来通信,channel可以是buffered,也可以是unbuffered。Buffered channel是异步的,unbuffered channel是同步的。Channel是first class type,可以被当作一个object传来传去,所以你可以把一个channel交给一个goroutine,让它通过这个channel把结果传回来。基本上,channel可以处理大部分goroutine同步的需要。Go当然也可以用底层的thread/lock之类的。有时候为了性能需要,可能也要用一下。
Java Memory Model (JMM)
为了说明线程和共享内存模式的问题,可以来看看Java memory model的演化中体现出来的保证共享内存模式正确的微妙性和困难性。
大家都知道Java中三个关键字final, synchronized和volatile。前两个比较容易理解,final是说被标记的变量是个常量,synchronized是表示一段代码由monitor保护,起到一个互斥的作用。至于volatile,是说被标记的变量不会被cache(register, L1/L2 cache or other buffers),直接写到内存。但由于硬件的复杂性,Java memory model并不是从一开始就把这几个关键字的真正用途实现正确了的。在Java 1.4之前,JMM有很多问题。
首先说一下导致问题的根源。几个问题,一是memory hierarchy,包括register,cache,buffer和RAM,一个变量赋值以后,很可能没有立即写到内存里去。二是硬件的分布性。在多个CPU/core的情况下,各个cpu/core可能有自己的cache,这就有了cache coherency的问题。一个线程给一个变量赋值,不见得另外一个线程就能马上看到。还有就是程序指令本身很可能被compiler重新排序来提供运行性能。由于Java是compile once, run everywhere,它就需要保证实现一个memory model,使得程序在任何系统上都运行正确。由于C/C++没有这样的保证,所以写多线程程序的时候要特别小心,很可能在一个硬件平台上运行正确的程序,到了另外一个硬件平台上就不对了。而这些因素使得实现一个跨平台的memory model很有挑战性。
先来看final。在Java 1.4之前,对final的实现不正确。一个final变量,创建的时候先有一个缺省值,然后才赋给初始值,这之间的间隔在多线程的情况下就会导致问题。看下面的例子:
String s1 = “/usr/tmp”;
String s2 = s1.substring(4)
正确的结果,s2应该是”/tmp”。但在Java 1.4之前,多线程情况下,s2可能是”/usr”。这是因为String的实现,有三个final域,包括长度和起始位置,但因为没有对多线程的保证,当String的构造函数被调用时,另一个线程可能会看到final域的缺省值,然后才看到初始值。JSR 133中的新的JMM,保证了这种情况不会出现。
再来看volatile。Volatile的含义很多程序员都不是很清楚。Volatile是说,被标记的变量的变化,应该立即被所有的观察者看到。但在实际对volatile的使用中,往往会出问题。比如下面的例子:
Map config;
volatile boolean initialized = false;
// in thread A
Config = new HashMap();
… // add something to config
Initialized = true;
// in thread B
while (initialized) {
… // use config
}
这里initialized被用作一个guard,保证我们能使用初始化以后的config变量。实际中呢,thread B有可能看到没有初始化过的config。这是因为volatile只保证initialized这个变量立即被其他的线程看到,但由于程序指令的乱序执行,并不能保证initialized赋值之后, config已经准备好了。在JSR 133中,加入了指令的排序,保证了这种情况不会发生。
最后一个是synchronized。表面上,synchronized意味着被保护的程序在一个 monitor里面运行,也就是说是线程互斥的。但实际上还有更深的含义。简单说,就是还有memory barrier的保护。这保证了在monitor上的线程可以以可预见的方式看到互相之间对内存变量的修改。而没有synchronized的代码就没有这种保证。一个例子就是常见的singleton;
Class someClass {
Private Resource resource = null;
Public Resource getInstance() {
If (resource == null) {
Resource = new Resource();
}
Return resource;
}
}
在多线程的情况下,需要给getInstance()加上synchronized。有人就会想优化一下,因为synchronized是有代价的。这个优化叫double-checked locking (DCL):
Class someClass {
Private Resource resource = null;
Public Resource getInstance() {
If (resource == null) {
Synchronized {
If (resource == null) {
Resource = new Resource();
}
}
}
Return resource;
}
}
改进就是先看一下是不是为空,空的话再做synchronized的操作。但DCL是不对的,因为第一个检查没有synchronized保护,很有可能另外一个线程在创建resource的时候,本线程就看到了,然后在resource的constructor被调用完之前就把resource返回了。这就有可能导致对未初始化的object的使用。DCL在新的JMM下也不行。
综合以上的例子,可以看到多线程共享内存的复杂性。写程序的时候一定要知其所以然,加上小心,才能尽量避免多线程编程的错误。