Go Routine intro.
About 1917 wordsAbout 6 min
Golang
2024-11-20
笔记记录了Goroutine的概念,特点,使用场景等等。
概述
来自
go
官方指南中的概述
Go 原生支持应用之间的通信和程序的并发。程序可以在不同的处理器和计算机上同时执行不同的代码段。Go 语言为构建并发程序的基本代码块是 协程 (goroutine) 与通道 (channel)。他们需要语言,编译器,和runtime的支持。Go 语言提供的垃圾回收器对并发编程至关重要。
协程是轻量的,比线程更轻。它们痕迹非常不明显(使用少量的内存和资源):使用 4K 的栈内存就可以在堆中创建它们。因为创建非常廉价,必要的时候可以轻松创建并运行大量的协程(在同一个地址空间中 100,000 个连续的协程)。并且它们对栈进行了分割,从而动态的增加(或缩减)内存的使用;栈的管理是自动的,但不是由垃圾回收器管理的,而是在协程退出后自动释放。
协程是通过使用关键字 go
调用(执行)一个函数或者方法来实现的(也可以是匿名或者 lambda 函数)。这样会在当前的计算过程中开始一个同时进行的函数,在相同的地址空间中并且分配了独立的栈,比如:go sum(bigArray)
,在后台计算总和。
协程的栈会根据需要进行伸缩,不出现栈溢出;开发者不需要关心栈的大小。当协程结束的时候,它会静默退出:用来启动这个协程的函数不会得到任何的返回值。
任何 Go 程序都必须有的 main()
函数也可以看做是一个协程,尽管它并没有通过 go
来启动。协程可以在程序初始化的过程中运行(在 init()
函数中)。
在一个协程中,比如它需要进行非常密集的运算,你可以在运算循环中周期的使用 runtime.Gosched()
:这会让出处理器,允许运行其他协程;它并不会使当前协程挂起,所以它会自动恢复执行。使用 Gosched()
可以使计算均匀分布,使通信不至于迟迟得不到响应。
传统用户线程的特点与限制
固定栈大小
- 用户线程的栈大小通常是固定的(例如 1 MB),需要在创建时预先分配。
- 如果线程过多,可能导致内存不足或资源浪费。
调度开销较大
- 用户线程依赖线程库(如 POSIX pthread)或操作系统调度,线程切换需要保存和恢复上下文,开销较高。
- 当线程数量较多时,调度器可能成为性能瓶颈。
同步复杂
- 用户线程之间的通信和同步需要依赖锁(如
mutex
)、条件变量等。 - 锁的使用容易导致死锁、竞争条件和性能问题。
Goroutine优势
极低的创建和调度开销
- Goroutine 由 Go runtime 管理,创建开销远低于传统用户线程。
- 初始栈仅约 2 KB,而传统线程通常需要分配 1 MB 或更多的固定栈空间。
- 栈会根据需要动态扩展,避免内存浪费和栈溢出。
高效的 M:N 调度模型
Go runtime 的调度器通过 M:N 模型,将大量 Goroutine 映射到少量操作系统线程上。 - M:N 模型: 多个 Goroutine 映射到多个操作系统线程。 - 调度器自动管理 Goroutine 的上下文切换,减少 CPU 和内存开销。 原生支持通信机制
提供基于 CSP(通信顺序进程) 的
channel
,用于 Goroutine 间的通信和同步:安全性: 避免共享内存带来的竞争问题。
简洁性: 简化代码逻辑,降低锁的使用频率。
轻量且易扩展
- Goroutine 可以轻松支持 成千上万个并发任务,而传统用户线程的数量通常受系统资源限制。
- Goroutine 的动态栈扩展使其适用于资源有限的环境。
自动内存管理
- Go 的垃圾回收器会帮助管理 Goroutine 的内存,开发者无需手动释放栈或管理其生命周期。
- 传统用户线程通常需要手动管理内存使用,容易导致泄漏。
Goroutine 的不足
调度依赖 Go runtime
- Goroutine 的调度完全由 Go runtime 管理,而非操作系统。
- 对于某些实时性要求较高的任务,可能不如传统线程精准。
并行受限于线程数
- Goroutine 依赖操作系统线程运行,若线程数受限,则 Goroutine 的并行能力也会下降。
- 可以通过
GOMAXPROCS
调整最大并行核心数,但受硬件限制。
调用非 Go 语言代码的影响
- 如果 Goroutine 调用阻塞的非 Go 语言代码(如 C 函数),可能阻塞整个操作系统线程,而非单个 Goroutine,降低并发效率。
并发能力
Go 协程 (Goroutines):
• Go 协程是 **并发** 的基本单元,能够被调度为真正的并行运行。
• Go 的运行时调度器通过将 Goroutines 映射到操作系统线程上,实现了多核并行执行。
• Goroutines 是为大规模并发任务设计的,轻量级且可以动态扩展栈,非常适合处理数十万甚至更多的并发任务。
其他语言的协程 (Coroutines):
• 协程通常只实现 **协作式并发**,不是真正的并行。
• 一个协程会通过显式的 **让出 (yield)** 控制权,转移到另一个协程。
• 它们更像是 “轻量级的函数生成器”,适合单线程环境下的任务切换,但不直接支持并行。
特性总结
Go 的协程(goroutine)是 Go 语言提供的一种轻量级并发机制,是编写并发程序的核心组件。它通过语言层面实现了高效的任务调度,使开发者可以轻松实现并发和并行操作。其本质上是一种用户线程。
特性 | Goroutine | 传统用户线程 |
---|---|---|
栈大小 | 动态调整,初始栈仅约 2 KB | 固定大小,通常为 1-8 MB |
内存使用 | 更轻量级,可以创建成千上万的 Goroutine | 占用更多内存,线程数受系统资源限制 |
调度方式 | Go runtime 的 M:N 调度,用户级调度 | 用户线程库实现 M:1 或 1:1 调度 |
上下文切换 | 由 Go runtime 调度,开销小 | 线程库或操作系统负责,开销较大 |
并行支持 | 可以利用多核,通过绑定操作系统线程实现并行 | 依赖线程库或操作系统实现并行 |
通信方式 | 提供原生的 channel ,简化同步 | 需要借助锁或信号量(如 mutex 、cond ) |
创建开销 | 极低,几乎可忽略 | 创建线程需要系统调用,开销较高 |
运行时依赖 | 完全依赖 Go 的 runtime | 依赖用户线程库和底层操作系统 |
适用场景
高并发任务
- 适合 I/O 密集型任务,如处理网络请求或构建微服务。
- 每个请求可以通过一个 Goroutine 处理,性能高效。
并行计算
- 适用于多核系统上的计算密集型任务,通过
sync.WaitGroup
或channel
同步结果。
定时任务与后台服务
- Goroutine 的轻量特性使其非常适合定时任务或后台任务。