1.3 线程的前世今生

线程是操作系统能够进行运算和调度的最小单元,在一个进程中可以创建多个线程,每个线程可以并行执行多个任务,并行执行的线程数量是由CPU的核心数量决定的。

1.3.1 大白话理解进程和线程

可能大家对这句话的理解还比较抽象,笔者将从进程到线程的整个过程做一个简单的分析。

我们平时使用Java语言写出来的程序是由一系列.java结尾的文件组成的,这些文件是存储在硬盘上的静态文件,通过Java虚拟机编译成和平台无关的字节码,也就是变成了.class结尾的文件。

当我们通过main()方法运行这个程序后,这些.class文件会被加载到内存中等待被执行,接着CPU开始执行这些程序的每一行指令,然后基于这些指令产生相应的结果,我们把这个运行中的程序称为进程。

假设在这个程序中,有一段逻辑是从磁盘上解析一个文件进行持久化操作,当CPU执行到从磁盘读取数据这个指令时,由于磁盘的I/O速度相比CPU的运算速度来说要慢很多,所以CPU在等待磁盘I/O返回的过程中一直处于闲置状态。CPU作为计算机的核心资源,被闲置显然是不合理的。

分时系统的出现解决了这个问题,分时系统是计算机对资源的一种共享方式,它利用多道程序和CPU时间片调度的方式使得多个用户可以同时使用一台计算机。

什么是多道程序呢?由于单个程序无法让CPU和I/O设备始终处于忙碌状态,所以操作系统允许同时加载多个程序到内存,也就是说可以同时启动多个进程,系统给这些进程分配独立的地址空间,以保证每个进程的地址不会相互干扰。

当CPU在执行某个进程中的指令出现I/O或其他阻塞时,为了提高CPU的利用率,操作系统会采用CPU调度算法把闲置的CPU时间片分配给第二个进程,当前进程运行结束后又会把CPU时间片分配给之前阻塞的进程来执行,从而保证CPU一直处于忙碌状态,整个调度过程如图1-1所示。

图1-1 CPU时间片的切换

在多核CPU架构中运行多个进程,从而实现多个进程的并行执行,一切看起来很美好,那么为什么又要有线程这种设计呢?

原因是进程本身是一个比较重的设计。首先,每个进程需要有自己的地址空间,并且每次涉及进程切换时,需要保存当前CPU指令的上下文,这使得资源的消耗及性能的损耗比较大。其次,对一个独立的进程来说,该进程内同一时刻只能做一件事,如果在这个进程中想实现同时执行多个任务并行执行,很显然是做不到的。最重要的是,当进程中某个代码出现阻塞时,会导致整个进程挂起,即便有些逻辑不依赖于该阻塞的任务也会无法执行。为了解决这个问题,人们把进程的资源分配和进程中任务调度的执行分开处理,因此形成了线程的概念。

引入线程的设计后,CPU的最小调度和分配单元就变成了线程,在一个进程中可以创建多个线程。因此,当出现上面描述的情况,即一个进程中存在多个任务时,我们可以针对每个任务分配独立的线程来执行,当其中一个任务因为阻塞无法执行时,其他任务不会受到影响。

除此之外,线程的好处还有很多。

• 由于线程不需要分配操作系统资源,所以它相对进程来说是比较轻的。

• 线程的切换只需要保存少量的寄存器内容,相比进程来说,资源耗费更小,因此效率更高。

• 一个进程中可以创建多个线程,同一个进程中多个线程的CPU时间片的切换并不会导致进程切换,而且还能实现单进程中多个任务的并行执行。

总结一下,进程和线程的主要区别是,操作系统的资源管理方式不同,进程有独立的地址空间,当一个进程崩溃后,不会影响其他进程。而线程是一个进程中不同的执行路径,它有自己的堆栈和局部变量,但是没有单独的地址空间。

1.3.2 线程的核心价值

在影响服务端的并发数中有两个指标是CPU核心数和应用中的线程数,它们是如何影响整体并发的呢?

我们知道,同一时刻能够同时运行多少个线程是由CPU的核心数来决定的,对同一个任务来说,单线程的执行和多线程同时执行相比,多线程同时执行的效率更高,这就意味着从用户发起请求到收到服务端的返回结果的耗时会大大缩短。这样,一方面能够提升用户的响应速度,另一方面能够快速释放资源,使得整体架构的并发性能得到一定的提高。

如图1-2所示,假设在一个用户注册的流程中,会涉及保存用户信息到数据库,以及发送注册成功的邮件通知,前者需要耗时3秒才能执行完成,后者需要耗时2秒,那么整个流程执行完成并返回结果给用户一共需要5秒,显然这个时间相对来说是比较长的。

图1-2 用户注册

如图1-3所示,我们使用线程优化了图1-2中的注册流程,当用户信息保存到数据库之后,直接返回给用户一个注册成功的结果,由于发送邮件这个流程和注册没有直接的关联性,所以可以采用异步线程来执行邮件发送。使用线程优化后,用户就可以在3秒内收到注册成功的通知。

图1-3 使用线程优化后的注册流程

上述这个案例,就利用了线程异步执行的特性。所谓异步,就是调用者发送一个任务执行指令后,不需要等待该指令的返回结果,而是可以继续执行后续的流程。这种异步特性本质上说也是一种并行处理方式,就是在一个进程中可以同时处理多个任务。

1.3.3 如何理解并发和并行

从操作系统层面来说,并发和并行可以表示CPU执行多个任务的方式。

并发:并发是指两个或多个任务在同一时间间隔内发生,比如在4核CPU上运行100个线程,由于核数限制,这100个线程无法在同一时刻运行,所以CPU只能采用时间片切换的方式来运行,如果这100个线程能够在1s内全部处理完成,那么我们可以认为当前的并发数是100。

并行:当有多个CPU核心时,在同一个时刻可以同时运行多个任务,这种方式叫并行。比如,4核CPU可以同时运行4个线程。

从宏观层面来说,一个系统能够处理的请求数即当前系统的并发数,但是这个并发数是由很多因素决定的,其中最主要的因素就是当前进程最多允许同时打开的连接数,在Linux系统中可以通过命令ulimit -n查看,假如得到的结果是1024,那么该进程能够并行处理的连接数就是1024。

总的来说,并行和并发的区别就是,多个人做多件事情和一个人做多件事情的区别。