架构设计:进程还是线程?是一个问题!

2009-02-22编程 编程.多线程 编程.架构

  就像莎士比亚的“To be, or not to be, that is the question”始终困扰着哈姆雷特,对于“进程还是线程?”这个问题,也经常困扰着那些进行软件架构设计的家伙。所以今天打算聊一下我对这个问题的体会。假如你还搞不清楚线程和进程的区别,请先找本操作系统原理的书好好拜读一下,再回来看帖。
由于这个问题很容易引发口水战,事先声明如下:多进程和多线程,无法一概而论地说谁比谁好。因此本帖主要描述特定场景(与我所负责的产品相关)下,进程和线程的权衡经验,仅供大伙儿参考。
  由于特定场景是本帖讨论的前提,先说说我目前负责的产品的特点:业务逻辑比较复杂、业务数据量比较大、对数据实时处理的性能要求比较高、对健壮性和安全性要求比较高、要求跨平台(包括操作系统、数据库)、某些情况下需要分布部署。
  上面说了一大堆,其实有不少的应用系统符合上述特点,比如:某些网络游戏服务器、某些金融行业的业务系统、某些电子商务的交易系统等等。如果你正在从事的是类似的应用系统的设计,希望我下面介绍的经验对你有帮助。

★进程【颗粒度】问题


  大伙儿应该明白,进程和线程都是处理并发(concurrency)的手段。对于上述这种比较复杂的系统,如果你企图全部用进程(见“注1”)或者全部用线程(见“注2”)来处理并发,估计会死得很难看。所以,关键问题就是如何在进程和线程之间进行平衡(也就是确定进程颗粒度的问题)。
  注1
  所谓“全部用进程”,就是所有的并发都使用进程实现(因此每个进程只有一个线程)。这种设计在某些平台上(比如 Windows)会导致严重的性能问题。
  注2
  所谓“全部用线程”,就是所有的并发都使用线程实现(因此整个系统只有一个进程)。这种设计的健壮性极差(一个致命错会导致整个系统崩溃),而且更别提分布部署了。

  我个人建议,尽量以业务逻辑的单元来划分进程。这样做的好处有如下几点:

★“以业务逻辑为单元”划分进程的好处


◇避免扯皮


  一般来说,某个固定业务逻辑的开发人员也是相对固定的。如果业务逻辑对应的某个进程崩溃了,测试人员容易快速定位肇事者,然后直接提交Bug给他/她。
  反之,一个进程搞得太庞大,N 多人掺和在里面,一旦进程崩溃了,相关编程人员之间很容易互相扯皮,不利于维护安定团结的局面;另外,由于测试人员经常搞不清楚 Bug 属于谁,经常给错 Bug,也容易制造人民内部矛盾。
  由此可以看出,【相对细】的进程颗粒度能够避免一些管理上的麻烦。由于 XXX 经常教导我们:“稳定压倒一切”,所以该优点列第一条。

◇健壮性、容错性


  一般来说,开发人员的水平参差不齐,优秀的毕竟是少数(具体参见“二八原理系列”的帖子)。所以难免会有菜鸟程序员搞出低级错误,而有些低级错误是致命的,会导致进程的崩溃。
  如果你是以业务逻辑划分进程,一个业务逻辑的进程崩溃,对其它业务逻辑的影响不大(除非是该业务逻辑的依赖方);因此就不会出现“全部用线程”导致的弊端。

◇分布式


  我常碰见的分布式部署需求,一般都是按照业务逻辑的维度来划分。比如系统中有一个认证模块,里面包含有敏感的用户认证信息。这时候客户就会要求把该模块单独部署在一台经过安全加固的主机中(以防阶级敌人搞破坏)。
  如果是以业务逻辑为单位划分进程,要满足上述的部署需求就相对容易了(只要再配合恰当的进程间通讯机制,下面会提到)。
  另外,支持分布式部署还可以顺带解决性能问题。比如某个业务逻辑模块特别消耗硬件资源(比如:内存、CPU、硬盘、带宽),就可以把它拿出去单独放一台机器上跑。

◇跨编程语言


  这个好处可能很多人容易忽略。一般来说,每个编程语言都有各自的优缺点。如果你通过业务逻辑划分进程,就可以根据不同的业务逻辑的特点来选择合适的编程语言。
  比如:对于性能敏感的模块,我就使用 C++ 搞定;而对于一些业务逻辑密集型的模块,则使用 Java 或 Python 开发。

★进程间通讯(以下简称 IPC)问题


  既然不可能把整个系统放入一个进程,那就必然会碰到 IPC 的问题。下面就来说一下该如何选择 IPC。
  各种操作系统里面,有很多稀奇古怪的 IPC 类型。由于要考虑跨平台,首先砍掉一批(关于 IPC 的跨平台问题,我在“跨平台开发”系列中会提到)。剩下的 IPC 类型中,能够进行数据传输的 IPC 就不多了,主要有如下几种:套接字(以下简称 Socket)、共享内存、管道、文件。
其中 Socket 是俺强烈推荐的 IPC 方式,理由如下:使用 Socket 可以天然地支持分布式部署;使用 Socket 可以比较容易地实现多种编程语言的混合(比如:C++、Java、Python、Flex 都支持 Socket);使用 Socket 还可以省掉了一大坨“锁操作”的代码。
  列位看官中,或许有人在担心 Socket 的性能问题,其实大可不必多虑。当两个进程在【本机】上进行 Socket 通讯时,由于可以使用 localhost 环回地址,数据不用经过物理网卡,操作系统内核还可以进行某些优化。这种情况下,Socket 相对其它几种IPC机制,不会有太大的性能偏差。
  最后再补充一下,Socket 方式也可以有效防止扯皮问题。举个例子:张三写了一个进程 A,李四写了一个进程 B,进程 A 通过 Socket 方式发数据给进程B。突然有一天,两个进程的通讯出故障了。然后张三就说是李四接收数据出错;李四就说张三发送数据出错。这时候怎么办捏?很简单,随便找个 Sniffer 软件当场抓一下数据包并 Dump 出来看,问题就水落石出了。

★为啥还要线程?


  上面说了这么多进程的好处,有同学要问了:“那线程有什么用捏?”总的来说,使用线程出于两方面的考虑:性能因素和编码方便。

◇性能因素


  由于某些操作系统(比如 Windows)中的进程比较重型,如果【频繁】创建进程或者创建大量进程,会导致操作系统的负载过高。举例如下:
  假设你要开发一个类似 Web Server 的应用。你针对每一个客户端请求创建一个对应的进程用于进行数据交互(是不是想起了古老的 CGI)。一旦这个系统扩容,用户的并发连接数一增加,你的应用立马死翘翘。
  上面的例子表明,跨平台软件系统的进程数要保持相对稳定。如果你的进程数会随着某些环境因素呈线性增长,那就相当不妙了(顺带说一下,如果线程数会随着环境因素呈线性增长,也相当不妙)。而根据业务逻辑的单元划分进程,顺便能达到“进程数的相对稳定”的效果。

◇编码方面


  由于业务逻辑内部的数据耦合比较紧密。如果业务逻辑内部的并发也用进程来实现,可能会导致大量的 IPC 编码(任意两个进程之间只要有数据交互,就得写一坨 IPC 代码)。这或许会让相关的编程人员怨声载道。
  当然,编码方面的问题也不是绝对的。假如你的系统有很成熟且方便易用的IPC库,可以比较透明地封装IPC相关操作,那这方面的问题也就不存在了。

  写到这里,看看篇幅有点超,就此打住。大伙儿如有不同看法,请到评论中拍砖。