玩转goroutine:Golang中对goroutine的理解
文章目录
- 前言
- 什么是 goroutine?
- goroutine 的基本工作原理
- 启动和执行goroutine
- goroutine与线程的区别
- 调度与上下文切换
- 同步机制
- Channel同步机制
- WaitGroup 等待执行
- Mutex(互斥锁)
- goroutine性能优化
- 总结
前言
Go语言的 goroutine 是一种轻量级的线程管理机制,它是 Go 提供的并发编程的核心之一。与传统的线程相比,goroutine 的创建和销毁开销非常小,可以轻松地处理成千上万的并发任务。理解 goroutine 的机制对于高效编写 Go 并发代码非常重要。
什么是 goroutine?
在 Go 中,goroutine 是一个由 Go 运行时(runtime)管理的轻量级线程。你可以使用 go 关键字来启动一个新的 goroutine,它会在后台并发执行某个函数。不同于传统的线程,goroutine 的创建和销毁非常快速且开销小。
go myFunction() // 启动一个 goroutine
goroutine 的基本工作原理
轻量级
:每个goroutine 都有自己的栈,但是初始栈非常小(通常是2KB),当需要更多的栈空间时,Go运行时会自动扩展栈的大小。多路复用
:Go使用一种被称为M:N线程模型的机制。这里的M
表示操作系统的线程数,N
表示goroutine的数目。Go运行时会将多个goroutine映射到少量的操作系统线程上,从而实现轻量级
的并发处理。调度器
:Go运行时有一个调度器负责管理所有的goroutine,调度器会在不同的goroutine之间切换,并确保它们并发执行。
启动和执行goroutine
通过go
关键字可以启动一个新的goroutine
:
func printNumbers() {
for i := 1; i <= 5; i++ {
fmt.Println(i)
}
}
func main() {
go printNumbers(). // 启动goroutine
fmt.Println("Main function is done.")
}
在以上的例子中,printNumbers
函数会在一个goroutine
中执行,而main
函数继续执行其他代码,输出的顺序上不确定的,因为goroutine
的调度顺序是由Go运行时决定的。
goroutine与线程的区别
内存开销
:goroutine
的内存开销远小于操作系统进程,通常一个goroutine
的栈空间是2KB
,而一个操作系统线程的栈通常是几MB
。创建开销
:创建goroutine
的开销要远低于创建操作系统线程,因为不需要与操作系统进行大量的上下文切换和资源分配。调度
:Go运行时调度器负责管理goroutine
的调度,多个goroutine
共享较少的操作系统线程,这样可以大大提高并发度。
调度与上下文切换
Go的调度器会定期将goroutine
切换到不同的CPU
核心上执行。调度器会根据以下原则来决定何时切换goroutine
:
- 如果
goroutine
执行的任务需要较长时间,调度器可能会进行主动切换,允许其他goroutine
执行。 - 当
goroutine
执行阻塞操作时(例如等待I/O),Go允许时会将其挂起,并运行其他goroutine
。
同步机制
虽然goroutine
是并发执行的,但在很多情况下我们需要协调它们的执行,避免并发冲突。Go提供了多种同步机制:
Channel同步机制
Go中的Channel
是一种非常常用的同步机制,用于在不同的goroutine
之间传递数据。通过channel
可以将数据从一个goroutine
发送到另一个goroutine
并实现同步。
func sayHello(c chan string) {
c <- "Hello from goroutine!"
}
func main(){
ch := make(chan string) // 创建一个channel
go sayHello(ch) // 启动goroutine
msg := <-ch // 从ch接收数据
fmt.Println(msg) // 输出从"Hello from goroutine!"
}
WaitGroup 等待执行
sync.WaitGroup
用于等待一组goroutine
执行完成,它通常用于在主程序中等待多个goroutine
完成工作。
import "sync"
func doWork(wg *sync.WaitGroup){
defer wg.Done() // 执行完成后调用Done(),通知WaitGroup
fmt.Println("Working...")
}
func main() {
var wg,sync.WaitGroup
wg.Add(2) // 计数器增加2
go doWork(&wg)
go doWork(&wg)
wg.Wait() // 等待所有goroutine执行完成
}
Mutex(互斥锁)
当多个goroutine
共享某个资源时,可能会发生数据竞争。Go提供了sync.Mutex
来保证在同一时刻只有一个goroutine
能访问共享资源。
import "sync"
var mu,sync.Mutex
var counter int
func increment() {
mu.Lock() // 上锁,防止其他goroutine 访问
counter ++
mu.Unlock() // 解锁,允许其他goroutine 访问
}
func main() {
for i := 0; i< 1000; i++ {
go increment()
}
time.Sleep(time.Second) // 等待goroutine执行完成
fmt.Println(counter)
}
goroutine性能优化
避免频繁创建销毁
:尽量避免频繁创建和销毁大量的goroutine
。可以通过连接池或工作池的方式来复用goroutine
。栈增长与收缩
:Go会自动调整goroutine
的栈大小,如果需要更大的栈,它会动态扩展栈道大小。理解栈道增长和收缩对优化内存管理非常重要。合理使用同步机制
:尽量减少对锁(Mutex)等同步机制的依赖,避免性能瓶颈。可以考虑使用channel
作为代替。
总结
goroutine
是Go的核心特性,允许你高效地进行并发编程。- Go使用了轻量级的线程管理,每个
goroutine
占用的内存非常小,创建和销毁开销低。 - Go的调度器将多个
goroutine
映射到较少的操作系统线程上,通过多路复用实现高效的并发。 - 通过
channel
、WaitGroup
、Mutex
等机制,可以确保goroutine
之间的同步和协作。