并发与协程

89 阅读5分钟

并发 (Concurrency)

一台主机上的资源总是有限的,CPU、内存、磁盘、网卡,在高并发场景下,这些都会成为制约程序性能的因素; 为了尽可能利用资源, 产生了以下概念:

进程(Process)

  • 进程是操作系统分配资源的基本单位,是一个正在执行中的程序实例。
  • 每个进程都有独立的地址空间、内存、文件句柄和其他系统资源。
  • 进程之间相互独立,通过进程间通信(IPC)来进行数据交换和通信。
  • 进程间的切换需要操作系统的介入,需要切换页表,刷新TLB,替换内存数据(虚拟内存), 寄存器等, 开销较大。

线程(Thread)

  • 线程是在进程内部执行的独立执行序列。
  • 同一进程中的多个线程共享相同的内存空间和资源,可以访问进程的全局变量和数据结构。
  • 线程之间可以通过共享内存进行通信和数据交换; 切换线程只需要刷新寄存器。
  • 线程之间的切换开销较小,由操作系统的线程调度器负责。

协程 Coroutine

  • 协程是一种轻量级的用户级线程

  • 协程之间可以在同一线程内进行切换,避免了系统切换上下文的开销。

  • 协程是一种协作式的并发模型, 因为不存在类似线程的系统的时间片阻塞, 协程必须由程序员显式地控制和调度, 即阻塞主动挂起切换协程, 在某些时刻主动交出控制权。

多进程实现并发

  • 安全, 某个进程出错, 其余进程可以继续工作
  • 切换进程内核的管理成本高
  • 进程间无法简单地通过内存同步数据

多线程实现并发

  • 因为线程之间共享内存空间, 所以任一线程出错都会导致该进程内所有线程出错。
  • 单个线程消耗的内存过多,比如 64 位的 Linux 为每个线程的栈分配了 8MB 的内存
  • 为了解决线程申请堆内存时,互相竞争的问题。每个线程预先在这个空间内申请堆空间还预分配了 64MB 的内存作为堆内存池。
  • 线程的切换需要操作系统的介入,需要保存和恢复线程的上下文信息,包括寄存器状态、栈信息等, 一次上下文切换的成本在几十纳秒到几微秒间

多协程实现并发

  • 节省 CPU, 协程切换不需要操作系统介入, 是由程序员显式控制的;

  • 节约内存, 协程切换可以以较小粒度进行, 不需要恢复整个线程的上下文。

  • 常用于处理I/O密集型操作和事件驱动编程,以实现高并发和高效的异步操作。

  • 无法利用多核处理器, 不适合计算密集型操作

结构化并发(Structured Concurrency)

Task: A unit of asynchronous work. 一系列异步操作的合集.

当操作变得复杂时, 多任务多线程管理变得非常复杂, 可能存在这些问题:

  • 这个 task 什么时候开始,什么时候结束?

  • 怎么做到当所有 subtask 都结束,main task再结束?

  • 假如某个 subtask 失败,main task 如何cancel 掉其他subtask?

  • 如何保证所有 subtask 在某个特定的超时时间内返回,无论它成功还是失败?

  • 更进一步,如何保证 main task 在规定的时间内返回,无论其成功还是失败,同时 cancel 掉它产生的所有 subtask?

  • main task 已经结束了,subtask 还在 running,是不是存在资源泄漏?

Structured Concurrency 核心在于通过一种 structured 的方法实现并发程序,用具有明确入口点和出口点的控制流结构来封装并发“线程”(可以是系统级线程也可以是用户级线程,也就是协程,甚至可以是进程)的执行,确保所有派生“线程”在出口之前完成。

协程(Coroutine)

函数

同步函数是一种特殊的协程, 只能在特定入口进入, 特定出口退出(return),

异步函数可以在函数内部中断执行并挂起, 在函数内部继续执行

函数 == 协程

func A {
   B()
}

func B() {}
func A() async {
    await B()
}

func funcB() async {}
B:
        ...
        ret
A:
        ...
        call  B
        leave
        ret

EIP(Instruction Pointer Register): 存储下一条将要执行的指令的内存地址。它指示了CPU当前正在执行的指令的位置。

call: 1. 将 eip 压栈 2. 往目标处跳转

ret: 从栈中恢复 eip

Q: 上述协程看起来是一个语法优化, 那可以套用线程/ 进程上吗?

A: 不行!!! 因为上述语法需要 await , 即程序员主动挂起该协程, 线程/ 进程需要系统切换时间片, 无法套用该语法;

定义和调用异步函数

  • async 标记该函数为异步函数, 可能在内部挂起/ 恢复
  • await 标记挂起点

Completion

func foo() {
  var val = 0
  A { a in // escaping
    B(a) { b in // escaping
        C(b) { c in // escaping
          val = c // c == 1
        } 
    }
  }
  print("val = ", val) 
}
// val = 0
func foo async {
  var val = 0
  let a = await A() // Task 1
  let b = await B(a) // Task 2
  val = await C(b) // Task 3 
  print("val = ", val)
}
// val = 1

协程: 主动挂起自己

Completion 不一定更坏!

  • 假如存在跨平台需求, completion 兼容性更好

References

docs.swift.org/swift-book/…

zhuanlan.zhihu.com/p/108759542