面试题-进程、线程、协程

301 阅读5分钟

对比

执行体地址空间调度方时间片调度主动调度
进程执行体之间不共享地址空间内核基于时钟中断syscall
线程执行体之间共享地址空间内核基于时钟中断syscall
协程执行体之间共享地址空间用户态基于时钟中断包装 syscall

进程、线程与协程

执行体的上下文,就是一堆寄存器的值。要切换执行体,只需要保存和恢复一堆寄存器的值即可

进程

  • 进程是在内核态下实现的
  • 是操作系统最基本的执行单元
  • 一个运行的程序(代码)就是一个进程,没有运行的代码叫程序
  • 进程拥有自己独立的内存空间,不同的进程间数据不共享
    • 全局变量、文件描述符

线程

  • 线程是在内核态下实现的
  • 相当于进程内的任务 一个进程可以处理多个子任务 那么可以使用多线程
  • 程序执行过程中的最小单元
  • cpu 调度执行的最小单位,也叫执行路径,不能独立存在,依赖进程存在,
    • 一个进程至少有一个线程,叫主线程,
    • 而多个线程共享内存(数据共享,共享全局变量),从而极大地提高了程序的运行效率。
    • 线程的调度是由操作系统负责

协程

  • 协程是在用户态下实现的,也被称为用户态线程 或者微线程
  • 是一种用户态的轻量级线程,协程的调度完全由用户控制。
  • 协程拥有自己的寄存器上下文和栈。
  • 协程调度时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操中栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快

使用场景

  • 自行调度自己写的程序,让一些代码块遇到IO操作时(因为 IO 操作不占用 CPU,且比较耗时),切换去执行另外一些需要 CPU 操作的代码块
    • 协程之间可以链接在一起以形成管道,一个协程可能会消耗输入数据并将其发送给其他协程处理程序, 最后可能会有另外一个协程显示结果

协程的优势

  • 协程的数量理论上可以是无限个,而且没有线程之间的切换动作,执行效率比线程高
  • 协程不需要 “锁” 机制,即不需要 lock 和 release 过程,因为所有的协程都在一个线程中
  • 相对于线程,协程更容易调试 debug,因为所有的代码是顺序执行的

多线程的场景

大量的来自客户端并行请求网络服务器,会有大量的请求包和服务器的返回包,这些都是网络 IO;在响应请求的过程中,往往需要访问存储来保存和读取自身的状态,这也涉及本地或网络 IO 这些IO都是并行的 但是提供这些IO是有成本的

  • 系统调用机制产生的开销;
  • 数据多次拷贝的开销(数据总是先写到操作系统缓存再到用户传入的内存)
  • 因为没有数据而阻塞,产生调度重新获得执行权,产生的时间成本
  • 线程的空间成本和时间成本(标准 IO 请求都是同步调用,要想 IO 请求并行只能使用更多线程)

epoll机制

  • 需要 IO 时登记一个 IO 请求,然后统一在某个线程查询谁的 IO 先完成了,谁先完成了就让谁处理
  • 目的是为了减少线程的数量

线程的时间成本

  • 执行体切换本身的开销,它主要是寄存器保存和恢复的成本,可腾挪的余地非常有限
  • 执行体的调度开销,它主要是如何在大量已准备好的执行体中选出谁获得执行权
  • 执行体之间的同步与互斥成本

线程的空间成本

  • 执行体的执行状态;
  • TLS(线程局部存储)
  • 执行体的堆栈
    • 默认情况下 Linux 线程在数 MB 左右,其中最大的成本是堆栈
    • 出于线程执行安全性的考虑,线程的堆栈不能太小

协程的目的

  • 回归到同步 IO 的编程模式
  • 降低执行体的空间成本和时间成本

1个完备的协程库你可以把它理解为用户态的操作系统,而协程就是用户态操作系统里面的 “进程”

goroutine

Go 语言里面的用户态 “进程” 称之为goroutine

  • 堆栈开始很小(只有 4K),但可按需自动增长;
  • 坚决干掉了 “线程局部存储(TLS)” 特性的支持,让执行体更加精简
  • 提供了同步、互斥和其他常规执行体间的通讯手段,包括大家非常喜欢的 channel;
  • 提供了几乎所有重要的系统调用(尤其是 IO 请求)的包装。

协程和多线程的区别

  • 协程为单线程

  • 协程由用户决定,在哪些地方交出控制权,切换到下一个任务