Goroutines是Go架构中内置的轻量级执行线程,从最初版本开始就为了实现高效的竞争性设计。历史上,轻量级线程的概念是为了避免系统线程的高开销,同时满足对可扩展服务器应用程序的高需求。Go最初是作为服务器和网络系统的语言而设计,数百万个任务必须并行处理。
问题:如果不控制goroutines的生命周期,不考虑它们的调度,以及不管理它们的完成,競爭會很快導致競态条件、死锁和内存使用率的增长。
解决方案:Goroutines通过关键字go启动。Goroutines的工作由Go调度器管理,使用M:N模型(M个操作系统线程服务于N个Go语言的goroutines)。管理生命周期时使用通道、WaitGroup、context和通道关闭的控制。
代码示例:
package main import ("fmt"; "time") func worker(id int) { fmt.Printf("Worker %d started\n", id) time.Sleep(time.Second) fmt.Printf("Worker %d done\n", id) } func main() { for i := 1; i <= 3; i++ { go worker(i) } time.Sleep(2 * time.Second) }
关键特点:
如果在main中不明确等待goroutine,它会总是执行吗?
不,main执行完毕后,进程会结束,而不管子goroutines的状态,可能并不是所有任务都会执行。
在循环中启动 go func(...) 是否保证每个goroutine都会获得其循环变量的独立值?
不,这会导致循环变量捕获的问题,goroutines可能会使用相同的切片/变量值。需要使用变量的复制,例如,将其作为参数传递:
for i := 0; i < 3; i++ { go func(n int) { fmt.Println(n) }(i) }
一个goroutine可以阻塞Go调度器并不让其他goroutines执行吗?
可以,如果其中启动了一个无限或非常重的循环且没有切换点(例如,没有时间或yield函数的调用),它可以锁定操作系统线程——虽然这与Go的“协作式多任务”理念相违背。例如,无阻塞的重函数:
func busy() { for { // 没有任何等待或阻塞调用 } }
在微服务中定期启动goroutine读取数据库,但在请求取消时忘记结束它。结果留下了“挂起”的goroutines,随着时间推移导致消耗整个内存。
优点:
缺点:
使用context来控制任务的取消,WaitGroup用于在停止应用程序之前管理所有goroutines的完成,而通道用于在执行者之间正确传递数据。
优点:
缺点: