并发(Concurrent) 不是并行 (Parallel),并发更关注的是程序的设计层面,并发的程序完全是可以顺序执行的,只有在真正的多核CPU上才可能真正地同时运行,并发编程最大的挑战是 并发控制(concurrency control):即确保不同计算逻辑之间交互或通信的正确顺序,并协调对共享资源的访问。并发控制需要考虑 竞争条件(race conditions)、死锁(deadlocks) 和 资源匮乏(resource starvation) 等问题。
并发编程一般有四种方式:多进程、多线程、协程和异步,常用的编程语言都有完善的并发编程机制来保证交互及通信(Interaction and Communication),不同的实现机制对代码的编写要求也各不相同,一些并发组件之间的通信对程序员是隐藏的,也有一些是需要通过编程显式处理的。
¶并发编程常见的分类
现代操作系统提供了三种基本并发技术:
- 进程。每个逻辑控制流都是一个进程,由内核来调度和维护。因为进程有独立的虚拟地址空间,想要和其它流通信,控制流必须使用某种显式的进程间通信(inter process communication, IPC)机制。
- I/O多路复用。在这种形式的并发编程中,应用程序在一个进程的上下文中显式地调度它们自己的逻辑流。逻辑流被模型化为状态机,数据到达文件描述符后,主程序显式地从一个状态转换到另一个状态。因为程序是一个单独的进程,所以所有的流都共享同一个地址空间。
- 线程。线程是运行在一个单一进程上下文中的逻辑流,由内核进行调度。你可以把线程看成是其他两种方式的混合体,像进程流一样由内核进行调度,而像 I/O多路复用流一样共享同一个虚拟地址空间。
¶共享内存(Shared memory)
共享内存是在进程间传递数据的高效方式。在共享内存模型中,并行进程共享它们可以一步读与写的全局地址空间。异步并发访问可能导致竞争条件,用来避免它们的机制如:锁
、信号量
和管程
(也被称为监视器,Monitor)。常规的多核处理器直接支持共享内存,很多并行编程语言和库在设计上利用了它,比如采用Fork-join模型的:Cilk、OpenMP和线程建造块。
¶消息传递(Message passing)
在消息传递模型中,并行进程通过消息传递相互交互数据。这种通信可以是异步的,就是说消息可以在接受者准备好之前发出;或是同步的,就是说消息发出前接受者必须准备好。
目前编程语言设计上有两个分支:
通信顺序进程(CSP)
:形式化了使用同步通信管道来连接进程的消息传递,并引出了重要的语言如:Occam、Limbo和Go。框架如Vert.xActor模型
:使用异步消息传递,被采用的语言设计有:D、Scala和SALSA。框架有Akka。
这种模型通常需要一个绿色线程或Free thread,如Fork-join的work stealing algorithm类似的管理级用户线程。
¶并发编程很难
并发编程要解决的问题,往往不仅仅是竞争、死锁。真实世界面临的问题远比想象中的要复杂:
- 内存泄露
- 野指针
- 系统负载
- 伪共享
- 数据一致性、操作原子性
- 异常
- 类型安全
- 弱引用
…
并且,基于共享内存模型的并发设计遇到的问题相比消息传递模型要多得多
¶Coroutine
Kotlin提供了Coroutine的一种技术,协程。协程类似于线程。不同的是,协程是协作式多任务,而线程是典型的抢占式的。它允许执行被挂起(suspend)和恢复(yield)。
¶Fiber
Fiber也是一种Coroutine的实现,不同的是Kotlin中的Coroutine主要是为解决了async的问题。两者都会使用到Trampoline的技术,而Fiber主要解决的是side effect问题。它的线程是“用户态”、“非抢占”的。任务调度不由CPU控制,而是由用户指定,因此,像ZIO框架中的Fiber的线程都是ForkJoin实现的。
因此,我们可以认为Fiber就是绿色线程。Fiber抽象为调度计算,和Akka的coordinator类似。Fiber也是一种线程实现,仅包含一个数据结构,但和系统/JVM的线程不同,它的分配和移除非常轻量。
基于Fiber的free thread的特殊,会有几点限制:
- 线程不能被block,因为它属于 schedulable computation
- 可以中断,因为它是用户态的。中断意味着它告诉
Executor
不需要再进行调度了 - 可以回收(finalizer),回收意味着,绑定在该Fiber上的资源会被close