由异步回调引发的线程/进程“血案”

时间:2022-08-27 16:15:56

引言

最近在做项目时涉及到异步调用,出于好奇就百度了下什么是“异步调用”,不想这个背后竟涉及到多线程的问题,于是乎关于进程与线程的问题如滔滔黄河之水奔流而来,一场血案(探索之旅)由此引发。

一、 进程/线程基本概念

1. 进程

进程:程序的一个执行实例,例如当你双击Microsoft Word图标时,你就开始运行的Word的进程,具有如下特性:

  • 系统进行资源分配的基本单位,拥有独立的虚拟地址空间,一个进程的崩溃,在保护模式下不会对其他进程产生干扰,多进程应用相对而言可用性更高;
  • 进程的创建及上下文切换开销比较大,所以不适合高并发应用。

2.线程

线程:建立在进程的基础上的一次程序运行单位,可以看成轻量级的进程,是为使应用充分利用CPU引进的程序处理机制,具有如下特性:

  • CPU调度的基本单位,多个线程共享所属进程的虚拟地址空间,一个线程的奔溃可能回导致整个程序的崩溃,因此,多线程应用可用性低于多进程应用;
  • 线程的创建及上下文切换开销比较小,适合高并发应用场景;

:上文中特别强调了进程/线程的创建对高并发应用抉择的影响,因为有些场景下,进程/线程数量是固定的,所以创建的开销是可以忽略不计的,所以,此时多进程和多线程需慎重选择。

二、 进程/线程上下文切换

上文中有提到线程是为使应用充分利用CPU引进的程序处理机制,为什么线程可以提高CPU利用率呢?我们知道系统在执行并发的多个任务时,是通过轮询的方式实现的,而在由一个任务切换到另一个任务时,必须保存相应的上下文环境,以确保下次执行时,从当前位置继续执行。对于多线程和多进程应用,其涉及的上下文环境存在一定的差别,导致性能上的差异。

1. 上下文

进程/线程上下文对比如下表所示:
由异步回调引发的线程/进程“血案”

进程上下文的多数信息都与地址空间的描述有关。进程的上下文使用很多系统资源,而且会花费一些时间来从一个进程的上下文切换到另一个进程的上下文。如果线程属于相同的进程,它们共享相同的地址空间,因为线程包含在它们所属于的进程的地址空间内。这样,进程需要恢复的多数信息对于线程而言是不需要的。

2. 过程

上下文切换可以认为是内核在 CPU 上对于进程(包括线程)进行以下的活动:

  • 挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处;
  • 在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复;
  • 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程;

区别:
进程上下文切换涉及如下两步:

  • 切换页目录以使用新的地址空间;
  • 切换内核栈和硬件上下文;

对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。所以明显是进程切换代价大。线程上下文切换和进程上下问切换一个最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。

另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲(processor’s Translation Lookaside Buffer (TLB))会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题。

LINUX完全注释中的一段话:
当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称 为该进程的上下文。当内核需要切换到另一个进程时,它需要保存当前进程的 所有状态,即保存当前进程的上下文,以便在再次执行该进程时,能够必得到切换时的状态执行下去。在LINUX中,当前进程上下文均保存在进程的任务数据结 构中。在发生中断时,内核就在被中断进程的上下文中,在内核态下执行中断服务例程。但同时会保留所有需要用到的资源,以便中继服务结束时能恢复被中断进程 的执行.

:以上探讨主要针对单核场景,多核场景下有些结论可能就不适用了。

三、 多线程意义

1.概念

多线程是指进程中包含多个线程。

2.意义

从宏观上来看,应用程序是通过抢占CPU资源来执行相关任务的;如果一个进程包含的线程比较多,其在抢占CPU资源时将有更大的获取概率。

另外,对于I/O密集的任务而言,相比于单线程,利用多线程可以提高CPU的利用率和程序的执行效率。因为,多线程场景下,一个线程在因为I/O操作而进入阻塞状态时,其它线程可以抢占CPU资源,进而提高CPU的利用率和程序的执行效率。

四、 多线程 VS 多进程

1. 创建

引用知乎中的一个观点:

  • 对于 Windows 系统来说,创建进程的开销很大,因此 Windows 鼓励大家在一个进程内做事。因此 Windows 多线程学习重点是要大量面对资源争抢与同步方面的问题。
  • 对于 Linux 系统来说,创建进程的开销很小,因此 Linux 鼓励大家尽量为每个任务创建一个进程。因此,Linux 下的学习重点大家要学习进程间通讯的方法。
  • 如果你是写服务器端应用的,其实在现在的网络服务模型下,开进程的开销是可以忽略不计的,因为现在一般流行的是按照 CPU 核心数量开进程或者线程,开完之后在数量上一直保持,进程与线程内部使用协程或者异步通信来处理多个并发连接,因而开进程与开线程的开销可以忽略了。

:此处作者未考虑上下文切换的问题,仅从进程/线程创建的角度考虑的问题。

2.切换

核切换:现代的体系,一般 CPU 会有多个核心,而多个核心可以同时运行多个不同的线程或者进程。当每个 CPU 核心运行一个进程的时候,由于每个进程的资源都独立,所以 CPU 核心之间切换的时候无需考虑上下文。当每个 CPU 核心运行一个线程的时候,由于每个线程需要共享资源,所以这些资源必须从 CPU 的一个核心被复制到另外一个核心,才能继续运算,这占用了额外的开销。换句话说,在 CPU 为多核的情况下,多线程在性能上不如多进程。(单核场景下,多线程上下文切换性能占优。)

五、异步调用

1. 概念

异步调用(asynchronous call):无需等待被调用函数的返回值就让操作继续进行的方法;

2. 优缺点

因为异步操作无须额外的线程负担,并且使用回调的方式进行处理,在设计良好的情况下,处理函数可以不必使用共享变量(即使无法完全不用,最起码 可以减少 共享变量的数量),减少了死锁的可能。当然异步操作也并非完美无暇。编写异步操作的复杂程度较高,程序主要使用回调方式进行处理,与普通人的思维方式有些出入,而且难以调试。

3. 应用场景

  • I/O操作:I/O操作不仅包括了直接的文件、网络的读写,还包括数据库操作、Web Service、HttpRequest以及Remoting等跨进程的调用。
  • 分布式:分布式系统比单机系统 函数调用的时间要长很多,一个是毫秒级或亚毫秒级的,一个是微妙级的,有100多倍之差,大大影响了处理器的性能;

4. 其他

常见的方法调用都是同步调用,这种调用方式是一种阻塞式的调用方式,即客户端(主调用方)代码一直阻塞等待直到被服务端(被调用方)返回 为止。这种调用方式相对比较直观,也是大部分编程语言直接支持的一种调用方式。但是,如果我们面对是基于粗粒度的服务组件,面对的是一些需要比较长时间才 能有响应的应用场景,那么我们就需要一种非阻塞式调用方式,即异步调用方式。

六、参考资料

  1. 进程上下文与线程上下文
  2. Linux进程上下文切换过程context_switch详解–Linux进程的管理与调度(二十一)
  3. 线程上下文切换与进程上下文切换
  4. 线程和进程的区别是什么?
  5. 多线程有什么用?
  6. 什么是异步调用