线程的坏处
在理解线程和进程之前我就已经看了《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而已,要深入还需要很多的功夫啊。