本文原文地址:GoLang协程Goroutiney原理与GMP模型详解
Goroutine是Go语言中的一种轻量级线程,也成为协程,由Go运行时管理。它是Go语言并发编程的核心概念之一。Goroutine的设计使得在Go中实现并发编程变得非常简单和高效。
以下是一些关于Goroutine的关键特性:
协程(Coroutine)是一种比线程更轻量级的并发编程方式。它允许在单个线程内执行多个任务,并且可以在任务之间进行切换,而不需要进行线程上下文切换的开销。协程通过协作式多任务处理来实现并发,这意味着任务之间的切换是由程序显式控制的,而不是由操作系统调度的。
以下是协程的一些关键特性:
Goroutin就是Go在协程这个场景上的实现。
以下是一个简单的go goroutine例子,展示了如何使用协程:
package main
import (
"fmt"
"sync"
"time"
)
// 定义一个简单的函数,模拟一个耗时操作
func printNumbers(wg *sync.WaitGroup) {
defer wg.Done() // 在函数结束时调用Done方法
for i := 1; i <= 5; i++ {
fmt.Printf("Number: %d\n", i)
time.Sleep(1 * time.Second) // 模拟耗时操作
}
}
func main() {
var wg sync.WaitGroup
// 启动一个goroutine来执行printNumbers函数
wg.Add(1)
go printNumbers(&wg)
// 主goroutine继续执行其他操作
for i := 'A'; i <= 'E'; i++ {
fmt.Printf("Letter: %c\n", i)
time.Sleep(1 * time.Second) // 模拟耗时操作
}
// 等待所有goroutine完成
wg.Wait()
}
我们定义了一个名为printNumbers的函数,该函数会打印数字1到5,并在每次打印后暂停1秒。然后,在main函数中,我们使用go关键字启动一个新的goroutine来执行printNumbers函数。同时,主goroutine继续执行其他操作,打印字母A到E,并在每次打印后暂停1秒。
需要注意的是,主goroutine和新启动的goroutine是并发执行的。为了确保所有goroutine完成,我们使用sync.WaitGroup来等待所有goroutine完成。我们在启动goroutine之前调用wg.Add(1),并在printNumbers函数结束时调用wg.Done()。最后,我们在main函数中调用wg.Wait(),等待所有goroutine完成。这样可以确保程序在所有goroutine完成之前不会退出。
协程是一种强大的工具,可以简化并发编程,特别是在处理I/O密集型任务时。
Goroutine的实现原理包括Goroutine的创建、调度、上下文切换和栈管理等多个方面。通过GPM模型和高效的调度机制,Go运行时能够高效地管理和调度大量的Goroutine,实现高并发编程。
当使用go关键字启动一个新的Goroutine时,Go运行时会执行以下步骤:
Go运行时使用GPM模型(Goroutine、Processor、Machine)来管理和调度Goroutine。调度过程如下:
从上图中看,有2个物理线程M,每一个M都拥有一个处理器P,每一个也都有一个正在运行的goroutine。P的数量可以通过GOMAXPROCS()来设置,它其实也就代表了真正的并发度,即有多少个goroutine可以同时运行。图中灰色的那些goroutine并没有运行,而是出于ready的就绪态,正在等待被调度。P维护着这个队列(称之为runqueue),Go语言里,启动一个goroutine很容易:go function 就行,所以每有一个go语句被执行,runqueue队列就在其末尾加入一个goroutine,在下一个调度点,就从runqueue中取出(如何决定取哪个goroutine?)一个goroutine执行。
P的数量可以大于器的CPU核心数?
在Go语言中,P(Processor)的数量通常等于机器的CPU核心数,但也可以通过runtime.GOMAXPROCS函数进行调整。默认情况下,Go运行时会将P的数量设置为机器的逻辑CPU核心数。然而,P的数量可以被设置为大于或小于机器的CPU核心数,这取决于具体的应用需求和性能考虑。
调整P的数量,可以使用runtime.GOMAXPROCS函数来设置P的数量。例如:
package main
import (
"fmt"
"runtime"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
// 模拟工作负载
for i := 0; i < 1000000000; i++ {
}
fmt.Printf("Worker %d done\n", id)
}
func main() {
// 设置P的数量为机器逻辑CPU核心数的两倍
numCPU := runtime.NumCPU()
runtime.GOMAXPROCS(numCPU * 2)
var wg sync.WaitGroup
// 启动多个Goroutine
for i := 1; i <= 10; i++ {
wg.Add(1)
go worker(i, &wg)
}
// 等待所有Goroutine完成
wg.Wait()
fmt.Println("All workers done")
}
在这个示例中,我们将P的数量设置为机器逻辑CPU核心数的两倍。这样做的目的是为了观察在不同P数量设置下程序的性能表现。
选择合适的P数量选择合适的P数量需要根据具体的应用场景和性能需求进行调整。以下是一些建议:
Goroutine的上下文切换由Go运行时的调度器管理,主要涉及以下步骤:
Goroutine什么时候会被挂起?Goroutine会在执行阻塞操作、使用同步原语、被调度器调度、创建和销毁时被挂起。Go运行时通过高效的调度机制管理Goroutine的挂起和恢复,以实现高并发和高性能的程序执行。了解这些挂起的情况有助于编写高效的并发程序,并避免潜在的性能问题。
当Goroutine执行阻塞操作时,它会被挂起,直到阻塞操作完成。常见的阻塞操作包括:
使用同步原语(如sync.Mutex、sync.WaitGroup、sync.Cond等)进行同步操作时,Goroutine可能会被挂起,直到条件满足。例如:
Go运行时的调度器会根据需要挂起和恢复Goroutine,以实现高效的并发调度。调度器可能会在以下情况下挂起Goroutine:
Goroutine的栈空间是动态分配的,可以根据需要自动扩展。Go运行时使用分段栈(segmented stack)或连续栈(continuous stack)来管理Goroutine的栈空间:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。