一、CSP模型
CSP(Communicating Sequential Processes)是一种形式化的并发模型,最初由 Tony Hoare 于1978年提出,为了描述并分析并发系统中进程之间的交互。Go 语言的并发模型大量借鉴了 CSP 的概念,并通过 goroutines 和 channels 实现了它。
在 Go 语言的 CSP 并发模型中,核心概念包括:
Goroutines
- Goroutine: 是 Go 中实现并发的基本单位,类似于轻量级线程。每个 Goroutine 在独立的执行路径上运行函数,但与操作系统线程不同,Goroutines 是由 Go 运行时管理的,并不是直接映射到内核线程上。
- 调度器: Go 运行时包含一个内部的调度器,它负责把 Goroutines 分配给可用的逻辑处理器(P),然后绑定到操作系统线程(M)上执行。
Channels
- Channel: 是 Goroutines 之间进行通信和同步的主要方式。它是一个管道,可以传递指定类型的数据。Channel 可以是缓冲的或非缓冲的,区别在于是否允许发送操作在没有对应的接收操作时仍然完成。
- 阻塞与同步: 默认情况下,发送操作和接收操作都是阻塞的。如果 Channel 空,接收者会阻塞等待;如果 Channel 满(对于非缓冲 Channel,即容量为零),发送者会阻塞等待。
- Select 语句: Go 提供了 select 语句,允许 Goroutines 同时等待多个 Channel 操作,并根据哪个 Channel 先准备好来选择相应的操作。
CSP 并发模型的要点:
-
并发组件作为独立单元运行:在 Go 中,这些并发组件是 Goroutines,每个 Goroutine 都有自己的执行路径。
-
消息传递而非共享内存:CSP 强调在并发单元之间通过消息传递进行通信,而不是共享内存。Go 中的 Channel 实现了这一点,强制进行数据的复制而不是共享。
-
同步通信机制:在 CSP 中,进程间的通信往往是同步的,意味着当一个进程发送消息时,它会阻塞直到另一个进程准备好接收消息。Go 的 Channel 同样遵循这一模式,尽管也支持缓冲 Channel 来允许一定程度的异步通信。
-
避免锁和竞态条件:因为 CSP 倾向于使用消息传递而非共享内存,它本质上避开了需要使用互斥锁来控制并发访问共享资源的需求。
在 Go 中,CSP 并发模型被认为是比较简洁和有效的,因为它让开发者能以顺序的思维来编写并发程序,同时可大幅减少死锁、竞态条件等并发问题的出现。此外,通过 Goroutines 和 Channels,Go 程序员可以构建复杂的并发结构,如管道、工作池以及其他协调并发任务的模式。
二、GMP模型
GMP 是 Go 语言运行时的调度模型,这个模型涉及三个主要的实体:Goroutine(G)、Machine(M)和Processor(P)。
Goroutine (G)
- Goroutine 是 Go 语言中的协程,是执行用户代码的实体。Goroutines 比线程更轻量级,可以快速创建和销毁。它们并不直接对应到操作系统线程,而是由 Go 运行时管理和调度。
Machine (M)
- Machine 表示真实的操作系统线程。Go 运行时会在需要执行 Goroutines 的时候创建 M。每个 M 都会关联一个 P 来获取 Goroutines 列表进行执行。
Processor §
- Processor 是逻辑处理器,代表了执行 Go 代码所需的资源。P 维护着一个本地的队列,用来存放等待运行的 Goroutines。每个 P 最多只能绑定到一个 M。P 的数量通常由 GOMAXPROCS 环境变量控制,也可以在程序运行时通过
runtime.GOMAXPROCS()
函数设置。
调度过程
-
启动:当 Go 程序开始运行时,它会根据 GOMAXPROCS 的值初始化相应数量的 P,并启动 M 来执行程序中的 Goroutines。
-
执行:每个 M 都会尝试从与之绑定的 P 的本地队列中取出 Goroutine 来执行。如果 P 的本地队列为空,M 可以尝试从其他 P 的本地队列或者全局队列中偷取(work stealing)Goroutines。
-
阻塞和唤醒:如果一个 Goroutine 因为某些原因(如等待 I/O)而阻塞,执行该 Goroutine 的 M 会将其置于等待状态,并尝试从 P 的队列中取出另一个 Goroutine 来执行。被阻塞的 Goroutine 在资源准备好后会被唤醒并放入某个 P 的本地队列中。
-
系统调用和网络轮询器:Go 运行时拥有一个网络轮询器(netpoller),它负责处理可能阻塞的系统调用,如网络 I/O。当 Goroutine 执行系统调用时,M 可以分离(detach)P 并附加到其他 Goroutines 上;一旦系统调用完成,轮询器会重新唤醒 G,并找一个空闲的 P 给它继续执行。
-
调度策略:Go 调度器使用了非抢占式的调度策略,意味着除非 Goroutine 显式地放弃控制(如通过调用
runtime.Gosched()
、发生阻塞的系统调用、channel 操作等),否则它们会一直运行,不会被强行抢占。
GMP 模型允许高效地管理大量的 Goroutines,提供了可扩展性和弹性,并最小化了操作系统上下文切换的开销。这个模型的设计使得 Go 能够在并发编程中保持简单和高效。