编程笔记 Golang基础 035 Goroutines
- 一、基本概念
- 二、应用示例
- 三、高度模型
一、基本概念
Goroutines 是 Go 语言中用于实现并发编程的关键特性之一,它是轻量级的线程或协程。Go 运行时可以高效地创建和管理数以千计的 Goroutines,并通过多核处理器进行并行执行,极大地提高了程序性能和资源利用率。
-
Goroutine 创建:在 Go 中,启动一个 Goroutine 非常简单,只需在函数调用前加上
go
关键字即可异步执行该函数。例如:go funcName(param1, param2)
或者直接定义匿名函数并启动:
go func() { // 这里是 Goroutine 代码块 }()
-
调度机制:Go 的运行时会自动调度这些 Goroutines 在多个操作系统的线程(M:N 调度模型)上执行,使得它们看起来像是同时运行的。
-
资源消耗:相比于操作系统级别的线程,Goroutines 的创建、销毁和上下文切换成本更低,因为这些都是由 Go 运行时内部优化完成的。
-
通信与同步:虽然 Goroutines 可以独立运行,但它们之间经常需要通信和同步。Go 提供了 Channels(通道)这一原生支持来安全地在 Goroutines 间传递数据和同步执行流程。
二、应用示例
下面是一个简单的 Goroutine 应用示例,展示了如何使用 Goroutines 和 Channel 进行并发计算:
package main
import (
"fmt"
)
// 定义一个计算任务的函数
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("Worker %d started job %d\n", id, j)
// 模拟耗时工作
result := j * j
results <- result
fmt.Printf("Worker %d finished job %d with result %d\n", id, j, result)
}
}
func main() {
// 创建两个 Channel,分别用于传递任务和接收结果
jobs := make(chan int, 100)
results := make(chan int, 100)
// 启动两个 Goroutine 来处理任务
go worker(1, jobs, results)
go worker(2, jobs, results)
// 发送一些任务到 Channel
for w := 1; w <= 5; w++ {
jobs <- w
}
close(jobs) // 发送完所有任务后关闭 jobs Channel
// 收取并打印结果
for a := 1; a <= 5; a++ {
fmt.Println(<-results)
}
}
在这个例子中,我们创建了两个 Goroutines 执行名为 worker
的函数,该函数从 jobs
Channel 接收任务并把计算结果发送到 results
Channel。主 Goroutine 负责向 jobs
发送任务并在完成后从 results
接收结果。
三、高度模型
GPM 是 Go 语言并发编程模型的简称,它由三个核心组件构成:
-
G(Goroutine):代表执行体或任务,每个 Goroutine 对应一个 G 结构体,这个结构体中包含了程序计数器、栈和调度信息等。Goroutines 是 Go 中轻量级的线程,创建和销毁成本较低,并由运行时系统负责调度。
-
P(Processor/上下文):处理器,是 Goroutine 执行的实际载体。每个 P 都会与一个内核线程(M)关联,并且维护了一个可运行的 Goroutine 队列。P 负责调度其队列中的 Goroutine 在关联的 M 上执行,并通过 Channel 与其他 P 进行协作和任务交换。
-
M(Machine/线程):机器,表示操作系统级别的线程。M 负责实际的计算工作,每一个 M 都会绑定到一个 P 上,执行该 P 的任务队列中的 Goroutine。当 M 执行完当前 Goroutine 后,会从 P 的队列中获取下一个待执行的 Goroutine,或者如果 P 的队列为空,则会尝试从全局队列或其他 P 获取任务。
应用示例:
虽然在编写 Go 程序时通常不会直接操作 G、P、M 这些底层对象,但可以编写一段简单的并发代码来展示它们在背后的工作原理。例如,我们可以启动多个 Goroutine 并使用 Channel 进行通信,这将间接体现 GPM 模型的作用:
package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for j := range jobs {
fmt.Printf("Worker %d received job %d\n", id, j)
// 模拟耗时工作
// ...
}
}
func main() {
jobs := make(chan int, 100)
var wg sync.WaitGroup
// 创建5个Goroutine作为工作者
for w := 1; w <= 5; w++ {
wg.Add(1)
go worker(w, jobs, &wg)
}
// 发送一些任务到Channel
for i := 0; i < 20; i++ {
jobs <- i // 将任务放入jobs Channel
}
close(jobs) // 发送完所有任务后关闭 Channel
// 等待所有worker完成任务
wg.Wait()
fmt.Println("All jobs done.")
}
在这个例子中:
- 我们创建了多个 Goroutines (
worker
) 来处理来自jobs
Channel 的任务。 - 当
main
函数中的 Goroutine 将任务发送至 Channel 时,Go 运行时调度器根据 GPM 模型自动将这些任务分配给不同的 Worker Goroutine 执行。 - 使用
sync.WaitGroup
来确保所有 Worker 完成任务后,主 Goroutine 才会继续执行。
虽然这里没有直接涉及到 P 和 M 的具体操作,但它们在后台默默地管理着各个 Goroutine 的创建、执行和切换。GPM 模型确保了即便有大量 Goroutine 在同一时间并发运行,也能高效地利用硬件资源,并且能够正确处理并发环境下的同步问题。