青训营X豆包MarsCode 技术训练营 - Go Routine | 豆包MarsCode AI 刷题

42 阅读6分钟

本文记录训练营语法基础课部分的相关内容,对于课程中讲的不够充分的地方,结合了go入门指南中的详细介绍进行了补充。笔记同步更新在我的博客

本篇讲述了golang中协程相关的内容。

概述

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,简化同步需要借助锁或信号量(如 mutexcond
创建开销极低,几乎可忽略创建线程需要系统调用,开销较高
运行时依赖完全依赖 Go 的 runtime依赖用户线程库和底层操作系统

适用场景

高并发任务

  • 适合 I/O 密集型任务,如处理网络请求或构建微服务。
  • 每个请求可以通过一个 Goroutine 处理,性能高效。

并行计算

  • 适用于多核系统上的计算密集型任务,通过 sync.WaitGroupchannel 同步结果。

定时任务与后台服务

  • Goroutine 的轻量特性使其非常适合定时任务或后台任务。