process vs. thread

进程是程序执行时的一个实例,即它是程序已经执行到何种程度的数据结构的汇集。从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位。

线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个进程由几个线程组成(拥有很多相对独立的执行流的用户程序共享应用程序的大部分数据结构),线程与同属一个进程的其他的线程共享进程所拥有的全部资源。

进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

进程和线程两个模型的差异在于, 进程更安全,一个进程完全不会影响另外的进程。所以这也是 unix 哲学里推荐的编程方法; 但是进程间通信比线程间通信的性能差很多,尤其是,如果这个是系统的关键部分,而又有大量数据的时候,所有的进程间通信方法都比线程间的通信慢很多。

所以通常情况下推荐多进程程序,就像 Nginx ,一个 master 多个 worker,进程间只进行有限的通信(传递命令而非数据)。多线程的典型例子是 unbound ,一个开源的递归 dns 服务器。它使用线程的理由也很充分:程序需要不停地向后方的授权 dns 请求数据,并传回给前方的模块。这个数据通信量大,性能要求又高,所以必须用多线程,如果是多个进程,那就要慢许多了。

使用线程的理由

使用多线程的理由之一是和进程相比,它是一种非常 “节俭” 的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种”昂贵”的多任务工作方式。

而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。

使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。

除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,天然有以下的优点:

  • 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。

  • 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。

  • 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

对比说明

从函数调用上来说,进程创建使用 fork() 操作;线程创建使用 clone() 操作。

Richard Stevens大师这样说过(大意):

  • fork开销很大,内存从父进程复制到子进程,所有的描述符都需要在子进程复制,以此类推。目前fork的实现使用一种 写时复制 的技术,该技术避免将父进程的数据空间复制到子进程,直到子进程需要自己的副本。但是不论怎样优化,fork都是昂贵的开销

  • 在fork之后,父进程和子进程之间的通信需要采用IPC(进程间通讯): 在fork之前父进程传递给子进程信息非常容易,这是因为子进程从父进程数据空间复制了一个副本,并且也复制了所有父进程的描述符。但是反过来,子进程要向父进程返回信息就需要大量的工作。

线程有助于解决上述2个问题:

  • 线程有时候被称为轻量级进程,线程的创建速度比进程创建快10到100倍

  • 一个进程的所有线程共享相同的全局内存,这使得线程间信息共享很容易,但是这种简单性带来的问题是同步问题

查看进程的线程

  • 通过 /proc/<进程ID>/task/ 下的文件名可以看到线程的 TID ,举例,检查 java 进程的线程:

通过 /proc/<进程ID>/task/ 下的文件名获取线程 TID
ls /proc/$(pidof java)/task/
  • 通过 ps 命令参数 -eLf 能够查看系统所有线程:

ps 参数 -eLf 可以查看所有线程
ps -eLf | less

这里参数含义:

  • -L 显示线程,即 LWPNLWP 列信息

  • -e 显示所有进程(操作系统的所有进程,而不是仅仅当前用户)

  • -f 采用 full-format 列出模式

输出显示类似:

ps 参数 -eLf 输出案例
UID          PID    PPID     LWP  C NLWP STIME TTY          TIME CMD
root           1       0       1  0    1 Aug06 ?        00:00:13 /sbin/init
root           2       0       2  0    1 Aug06 ?        00:00:00 [kthreadd]
root           3       2       3  0    1 Aug06 ?        00:00:00 [rcu_gp]
...
root        1984       2    1984  0    1 Aug06 ?        00:00:00 [nvidia]
root        1985       2    1985  0    1 Aug06 ?        00:00:03 [nv_queue]
grafana     2001       1    2001  0   57 Aug06 ?        00:00:00 /usr/share/grafana/bin/grafana server --config=/etc/grafana/grafana.ini --pidfile=/run/grafan
grafana     2001       1    2389  0   57 Aug06 ?        00:00:10 /usr/share/grafana/bin/grafana server --config=/etc/grafana/grafana.ini --pidfile=/run/grafan
...

这里的:

  • 第4列 LWP 表示轻量级进程 Light Weight Process ,也就是线程 TID

  • 第6列 NWLP 就是表示 Number of Threads (线程数量)

  • ps 命令可以检查指定进程的线程,非常重要的命令:

检查指定进程的线程 重要命令
ps -T -p <PID>

输出显示类似:

检查指定进程的线程输出案例
   PID   SPID TTY          TIME CMD
 39112  39112 ?        00:00:00 rund-1f1b78d6
 39112  39115 ?        00:00:00 tokio-runtime-w
 39112  40565 ?        3-11:35:27 vmm_master
 39112  41223 ?        00:00:00 blk_iothread_q0
 39112  43205 ?        39-17:59:40 fc_vcpu0
 39112  43206 ?        34-22:50:52 fc_vcpu1
 39112  43207 ?        34-23:18:56 fc_vcpu2
...

可以看到,这里根据第5列 线程命令 进行统计,就能找出哪个命令大量出现线程泄漏:

统计指定进程的哪个线程出现泄
ps -T -p <PID> | awk '{print $5}' | sort | uniq -c | sort -n -k1

输出类似:

统计指定进程的哪个线程出现泄
...
      3 listener_loop
      3 reaper
      3 rund-1f1b78d6
      4 prealloc-memnum
  23758 client_handler
  • 通过 pstree 命令

使用 pstree 可以查看某个进程PID的所有线程
# 这里检查grafana进程的线程情况
pstree -pau -l -g -s 2001

输出案例显示( Grafana通用可视分析平台 ):

使用 pstree 可以查看grafana进程对应所有线程
systemd,1,1
  └─grafana,2001,2001,grafana server --config=/etc/grafana/grafana.ini --pidfile=/run/grafana/grafana-server.pid --packaging=deb cfg:default.paths.logs=/var/log/grafana cfg:default.paths.data=/var/lib/grafana cfg:default.paths.plugins=/var/lib/grafana/plugins cfg:default.paths.provisioning=/etc/grafana/provisioning
      ├─pcp_redis_datas,4012,2001
      │   ├─{pcp_redis_datas},4013,2001
      │   ├─{pcp_redis_datas},4014,2001
      │   ├─{pcp_redis_datas},4015,2001
      │   ├─{pcp_redis_datas},4016,2001
      │   ├─{pcp_redis_datas},4018,2001
      │   ├─{pcp_redis_datas},4019,2001
      │   ├─{pcp_redis_datas},4020,2001
      │   └─{pcp_redis_datas},4021,2001
      ├─{grafana},2389,2001
      ├─{grafana},2390,2001
      ├─{grafana},2391,2001
      ├─{grafana},2394,2001
      ...

备注

对于系统中异常的线程数量,请检查 线程数量统计 以及是否存在线程泄露问题

参考