并发 (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 兼容性更好