进程、线程和协程

149 阅读5分钟

进程和线程

我们先从进程和线程的区别入手,从区别中一点点去分析进程和线程究竟是什么。直接先说结论:

  • 进程是资源分配的最小单位,线程是程序执行的最小单位
  • 进程之间相互独立,线程共享进程的资源
  • 进程之间通信复杂,线程之间通信简单
  • 进程的创建和切换消耗资源比较大,线程的创建和切换消耗资源比较小

我们主要针对前三点来进行详细的解释和说明。

程序是如何运行的

进程是资源分配的最小单位,线程是程序执行的最小单位。如果要说明进程和线程,那就要来看程序究竟是如何执行的。

进程是一个应用程序,比如微信。可以理解为在磁盘中的一个文件。当我们点击程序的时候,也就是真正程序的执行,会将这个程序加载到内存中,也就是为这个程序分配内存空间。内存空间存储的是什么呢?主要就是指令和数据。指令可以理解为 a = 2 + 3。数据为 2 和 3。

线程指的是程序执行的最小单位。程序执行的过程实际是通过线程去执行的,cpu 会轮训不同的线程,执行线程里面的指令。线程运行的本质是函数的执行, 函数的执行总有一个源头,这个源头就是所谓的入口函数,CPU 从入口函数开始执行并形成一个执行流,这个执行流就是线程。

首先cpu 中的程序计数器(pc:programming count)会记录执行到指令,会将数据加载到寄存器中,由运算器去执行。比如执行 a=2+3 执行完毕会通过 IO Bridge 写回到内存中。在 Linux 操作系统中,并没有单独为线程设计调度算法和数据结构,区别就是创建时调用 clone 函数是否指定贡共享地址等资源。

Java 中的线程与内核线程是 1:1 的关系。

线程私有资源

线程共享进程的资源,进程之间的资源是相互独立的。那么线程到底共享进程中的哪些资源?关于这个问题,我们先来看有哪些资源是线程私有的,哪些资源是线程间共享的。

线程私有的可以统称为线程上下文(thread context) ,包括线程的栈区,程序计数器,栈指针以及函数运行使用的寄存器。

函数运行的信息保存在栈帧中,栈帧中包括函数的返回值,调用其他函数的参数,函数使用的局部变量以及该函数使用的寄存器信息。

CPU 的执行指令的信息保存在一个叫做程序计数器的寄存器中,由于 CPU 是轮训区执行线程的,当发生线程切换的时候,需要保留上一个线程执行的行数。

同时函数运行需要额外的寄存器来保存一些信息,像部分的局部变量等,这些寄存器也是线程私有的。

线程共享资源

线程共享进程的地址空间。地址空间是指代码区、数据区、堆区和栈区等。

进程地址空间的代码区,保存的是编译后的可执行机器指令。这些机器指令是从可执行文件中加载到内存。线程之间共享代码区,意味着程序中的任何一个函数都可以放到线程中区执行,不存在某个函数只能被特定线程执行的情况。

进程地址空间的数据区,存放的就是全局变量。数据区的全局变量有且仅有一个实例,所有的线程都可以访问到该全局变量。Java 中的全局变量存放在方法区中。

进程之间的通信

管道: 分为无名管道和命名管道。无名管道在父子进程之间通信(在 Linux/Unix 系统中,调用 fork() 函数创建的进程为子进程,调用的线程称为父进程)。命名管道允许两个不相关的进程进行通信,比如 cat xx.txt | grep -n 'xxx' 这个'|'可以看作一个单向的匿名管道,使得前后两个进程之间进行通信,前一个命名的输出作为后一个命令的输入。

共享内存: 多个进程共享一块内存区域,所有进程都可以去读写数据,比如 Redis。

信号: 用来控制多个进程对共享资源的访问,防止某进程在访问共享资源的时,其他进程也访问该资源。

消息队列: 在操作系统内核中维护一个消息队列,进程通过读写消息进行通信。

套接字: 不在一个主机或者夸网络进行通信。

信号量: 通常用于通知进程发生了某些特定的事件或异常,linux常见的信号如信号9:SIGKILL强制终止进程,信号2:SIGINI中断信号,按ctrl c产生,终止进程 <font style="color:rgb(31, 35, 40);">6</font>socket套接字 是TCP/IP通信的基本操作单元,用于客户端和服务端之间的通信

协程

协程(Coroutine)是一种比线程更轻量级的并发单元。与线程和进程不同,协程是由程序自身来管理的,而不是由操作系统管理。协程可以在一个线程内进行切换,不需要像线程那样频繁地上下文切换,从而提高了性能。

在 Go 语言中,协程使用 GMP 模型进行协程调度。