上一次我们说到,在多核或集群的环境下,可以提高系统整体的吞吐能力,这种架构的设计,和语言是无关的,但是有些语言,具有更好的适应并发环境编程的能力。我在这里把编程语言分四类来讲述它们的差异(为什么只分四类,因为我这里是砖,要等你的玉来补充不是吗)。
第一类,单进程解释语言python, ruby, node.js等
这类解释语言通常提供极高的开发效率,和相对较差的执行效率,在多核与并发的世界,它们不提供任何支持。执行一段代码时,无法使用到第二个cpu内核。所以执行这类语言开发的服务,想要利用所有的cpu,只能依靠架构和部署。
在多核和集群的环境下,这类语言的部署是类似的,就是启动多个进程,各自独立地响应服务请求,来提升系统整体的并发吞吐能力,对外接口方面,需要硬件或者软件的负载均衡代理层。如果有进程内的可变缓存对象,开发时需要考虑数据同步。
第二类,共享内存的多线程语言java, .net等
这类语言在设计之初都注重性能,具有较高的执行效率。在单核的年代,他们提供了创建操作系统线程的能力,可以在一个运行进程内,充分利用cpu的运算能力(当一个空闲线程等待时,其它的线程可以运行)。当多核cpu发明以后,那些支持多线程的软件,自动就具有了支持多核的能力。然而多核出现是在这类语言发明之后,真正的并发执行发生后,原先单核环境下不会发生的问题暴露出来,于是java和.net纷纷升级语言,提供补丁,以更好的支持多核并发的环境。
那么,并发执行暴露出来的是什么问题。通常是因为并发执行的进程访问了共享的内存,由于读写的次序不可预料,会产生不可预料的结果。怎么解决这个问题,java和.net提供的方法是加锁,就是到一个进程(线程)访问共享内存时,不允许其它进程(线程)访问。写加锁的代码对程序员的要求很高,一不小心就会发生死锁,而一旦发生死锁,排查错误非常困难。
在架构方面,在多核的单台服务器环境下,不需要运行多个进程也能提高吞吐能力,简化了部署。在集群环境下,和第一类语言相同。
第三类,不共享内存的多线程语言Erlang
Erlang诞生已经二三十年,设计之初,是为集群设计,提供了集群与单机一致的开发方法,从语言层面,消除了单机和集群的差异。用Erlang在单机开发的软件,可以轻松扩展运行在整个集群上。这是架构层面的简化。
Erlang是函数式语言,变量只能赋值一次,然后不可改变,同时,Erlang不允许进程间共享数据,从语言层面避免的并发编程最容易引起的错误。因为进程间不允许共享数据,所以Erlang也不需要加锁解锁的语句,我猜语言本身在底层实现用到了加锁原语,但程序员可以和繁复的锁告别,是足够幸福的。每个进程都可以安全的并发执行,但在进程内部,所有的操作都是串行的。往好处想,既享受到了多核和集群的好处,又避免了产生编程错误的可能,正是目前Erlang持续升温的原因。不足的地方是,不适合某些场景的应用,比如缓存服务,当我有一大块内存提供缓存服务时,只能有一个进程来读写这块内存,无法进一步提升服务性能。
第四类,为并发设计的混合式语言Golang
在介绍Golang之前,先总结Erlang适应多核编程的特点
1.单个服务能利用到多核
2.进程间不共享内存
3.进程间用消息通讯,不用加锁机制
4.进程间可以跨服务器通讯
之前我们说的第一类语言比如python,不能做到第一点。第二类语言java和.net,不能做到第三点,所有的语言都不能做到第四点(需要额外开发,不能在语言内部支持)。
如果以Erlang的思想来写Golang的代码,可以做到1,2,3点。你大概能知道Golang是怎么回事了。
Go语言的特色是,很多事情你都“可以”做。可以进程间使用消息管道来替代锁,也可以使用锁。可以使用函数式编程,也可以使用面向对象开发。可以共享内存,当然开发人员可以选择不共享内存。可以方便创建多个进程来并发执行,也可以指定最多的并发数目,限制cpu资源消耗。
参考Erlang的设计思想,我们开发Go语言应用时,可以做到“1”和“3”,便利地使用多核来提高性能,同时降低开发门槛。在我们《架构篇》中提到的场景,网页服务的场景可以做到“2”,进程间不共享内存;而缓存服务的场景,我们可以使用多进程来访问读取共享缓存,提高吞吐能力,而同时保证只存在一个进程,来修改共享缓存,避免写入冲突。
注意,这里有个边界状况,你需要留意,而我在这里忽略了。也就是写入操作完成一半时,可能被读取进程读到不完整的数据,在我这里的场景,并不太在乎。如果一定要保证读取数据一致,将不得不引入加锁机制,这是多么可怕。
转载于:https://blog.51cto.com/liuxp0827/1344915