Go 语言中的 Goroutines 是一种轻量级的线程,它允许你以非常低的成本并发执行多个函数或方法。Goroutines 是 Go 并发模型的核心组成部分,与 channels 一起使用可以实现高效的并发编程。
什么是 Goroutines?
1.内存占用小
初始堆栈大小:每个 Goroutine 的初始堆栈大小非常小,通常是 2KB(Go 1.4 及以后版本)。相比之下,一个典型的线程可能需要几十 KB 到几 MB 的堆栈空间。
动态调整:Goroutine 的堆栈是动态调整的。当 Goroutine 需要更多堆栈空间时,Go 运行时会自动增加堆栈大小;当 Goroutine 结束时,堆栈空间会被回收。
2.创建和切换成本低
创建成本:创建一个新的 Goroutine 的成本非常低,通常只需要几个微秒。这使得你可以轻松地创建成千上万个 Goroutines。
上下文切换:Goroutine 的上下文切换由 Go 运行时管理,而不是操作系统。这意味着上下文切换的成本更低,因为不需要涉及内核态和用户态之间的切换。
3.多路复用到少量操作系统线程
调度器:Go 运行时有一个自己的调度器,它将多个 Goroutines 多路复用到少量的操作系统线程上。这样可以减少线程的开销,并且更高效地利用 CPU 资源。
并发模型:通过多路复用,Go 可以在单个操作系统线程上运行多个 Goroutines,从而实现高效的并发执行。
4.无阻塞 I/O
非阻塞 I/O:Go 的标准库提供了许多非阻塞 I/O 操作,这些操作可以与 Goroutines 结合使用,从而避免了传统的线程阻塞问题。例如,net/http 包中的 HTTP 请求处理就是基于 Goroutines 和非阻塞 I/O 的。
具体示例
如何创建 Goroutines
要创建一个 Goroutine,只需在函数调用前加上 go
关键字。例如:
package main
import (
"fmt"
"time"
)
// say 函数接收一个字符串参数 s,并在接下来的500毫秒内,每100毫秒打印一次该字符串。
// 该函数主要用于在一段时间内重复输出某个消息,用于示例演示。
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond) // 暂停100毫秒以模拟延迟
fmt.Println(s) // 打印传入的字符串参数
}
}
func main() {
go say("world") // 在一个新的Goroutine中调用say函数,传入参数"world"
say("hello") // 在当前Goroutine中调用say函数,传入参数"hello"
}
在这个例子中,say("world")
在一个新的 Goroutine 中运行,而 say("hello")
在主 Goroutine 中运行。由于主 Goroutine 和新创建的 Goroutine 同时运行,所以你会看到 “hello” 和 “world” 交替打印。
Goroutines 的生命周期
在 Go 语言中,Goroutines 的生命周期管理是并发编程的一个重要方面。理解 Goroutines 的生命周期可以帮助你更好地设计和调试并发程序。以下是 Goroutines 生命周期的详细解释:
1. 创建
-
启动:使用
go
关键字可以启动一个新的 Goroutine。例如:go someFunction()
这会立即在一个新的 Goroutine 中异步执行
someFunction
。 -
初始堆栈大小:每个新创建的 Goroutine 都有一个初始堆栈大小(通常是 2KB),这个堆栈大小可以根据需要动态增长或收缩。
2. 执行
-
并发执行:一旦 Goroutine 被启动,它就会开始并发执行。多个 Goroutines 可以并行运行,具体取决于可用的 CPU 核心数和 Go 运行时调度器的决策。
-
调度:Go 运行时有一个自己的调度器,它负责将 Goroutines 分配到操作系统线程上。调度器会根据 Goroutines 的状态和优先级进行调度。
-
上下文切换:当一个 Goroutine 需要等待 I/O 操作或其他阻塞操作时,调度器会将其挂起,并调度其他可运行的 Goroutines。这种上下文切换非常高效,因为它是用户态的,不需要涉及内核态的切换。
3. 结束
-
函数返回:当 Goroutine 中的函数返回时,该 Goroutine 就结束了。如果函数返回了一个错误,你可以通过错误处理机制来捕获和处理这些错误。
-
显式结束:有时你可能需要显式地结束一个 Goroutine。虽然 Go 语言本身没有提供直接终止 Goroutine 的方法,但可以通过一些技巧来实现这一点,例如使用
context
包中的Context
来传递取消信号。
4. 等待所有 Goroutines 完成
-
同步点:主 Goroutine 可能需要等待所有子 Goroutines 完成后再继续执行。为此,可以使用
sync.WaitGroup
或者channel
来实现同步。-
使用
sync.WaitGroup
:package main import ( "fmt" "sync" "time" ) // worker 函数接收两个参数: // - id: 工作者的唯一标识符 // - wg: 一个 sync.WaitGroup 指针,用于等待所有工作者完成 // 该函数模拟了一个工作者的行为,在启动和完成时分别打印消息,并在中间暂停100毫秒以模拟工作时间。 func worker(id int, wg *sync.WaitGroup) { defer wg.Done() // 完成后通知 WaitGroup fmt.Printf("Worker %d starting\n", id) // 打印工作者启动消息 time.Sleep(100 * time.Millisecond) // 暂停100毫秒以模拟工作时间 fmt.Printf("Worker %d done\n", id) // 打印工作者完成消息 } func main() { var wg sync.WaitGroup // 初始化一个 WaitGroup numWorkers := 5 // 设置工作者的数量 for i := 0; i < numWorkers; i++ { wg.Add(1) // 增加 WaitGroup 的计数 go worker(i, &wg) // 在一个新的 Goroutine 中启动工作者 } wg.Wait() // 等待所有工作者完成 fmt.Println("All workers are done.") // 打印所有工作者完成的消息 }
-
使用
channel
:package main import ( "fmt" "time" ) // worker 函数接收两个参数: // - id: 工作者的唯一标识符 // - ch: 一个整型通道,用于传递完成信号 // 该函数模拟了一个工作者的行为,在启动和完成时分别打印消息,并在中间暂停100毫秒以模拟工作时间。 // 完成后将工作者的ID发送到通道 ch 中。 func worker(id int, ch chan int) { fmt.Printf("Worker %d starting\n", id) // 打印工作者启动消息 time.Sleep(100 * time.Millisecond) // 暂停100毫秒以模拟工作时间 fmt.Printf("Worker %d done\n", id) // 打印工作者完成消息 ch <- id // 将工作者的ID发送到通道 ch 中 } func main() { ch := make(chan int, 5) // 创建一个缓冲通道,容量为5 for i := 0; i < 5; i++ { go worker(i, ch) // 在一个新的 Goroutine 中启动工作者 } for i := 0; i < 5; i++ { <-ch // 从通道 ch 中接收完成信号 } fmt.Println("All workers are done.") // 打印所有工作者完成的消息 }
-
5. 资源回收
- 垃圾回收:当一个 Goroutine 结束时,其占用的资源(如堆栈空间)会被自动回收。Go 的垃圾回收机制会处理这些资源的释放,确保内存不会泄漏。
6. 错误处理
- 错误处理:Goroutines 应该适当地处理错误,并且最好能够通知调用者。可以使用
error
类型、panic
和recover
或者channel
来传递错误信息。
总结
Goroutines 的生命周期包括创建、执行、结束和资源回收。通过合理的设计和使用 sync.WaitGroup
或 channel
,可以有效地管理 Goroutines 的生命周期,确保程序的正确性和性能。理解和掌握这些概念对于编写高效的并发程序至关重要。