GO语言并发编程入门:Goroutine、Channel、Context、并发安全、GMP调度模型
1.GO并发介绍
并发:多线程程序在一个核的cpu上运行。 并行:多线程程序在多个核的cpu上运行。 由上可知并发不是并行,并行是直接利用多核实现多线程的运行,并发则主要由切换时间片来实现”同时”运行,go可以设置使用核数,以发挥多核计算机的能力。
Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。Go语言的并发编程特点主要体现在Goroutine协程和Channel通道的使用上。
- Goroutine协程:Goroutine是Go语言中的并发执行单位。它是一种轻量级的协程,由负责整个Go程序的执行的底层系统组件Go运行时(Go runtime)调度和管理。在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。与传统的线程、协程相比,Goroutine的创建和销毁代价非常小,可以高效地创建大量的Goroutine,Goroutine 奉行通过通信来共享内存,而不是共享内存来通信。Goroutine4~5KB的栈内存占用和由于实现机制而大幅减少的创建和销毁开销是go高并发的根本原因。
- Channel通道:通道是Goroutine之间进行安全通信和数据共享的机制。它提供了同步和互斥的功能,确保数据的有序传输和访问。通过通道,不同的Goroutine可以安全地进行数据交换和共享状态。
Goroutine是一种特殊的协程,这是因为普通的协程和操作系统线程是多对一的关系,而在Go语言中,Goroutine和操作系统线程是多对多的关系。具体来说:
- 一个操作系统线程(OS Thread)可以对应多个Goroutine。
- Go程序可以同时使用多个操作系统线程,这使得Go语言能够充分利用多核处理器的计算能力。
- Go运行时调度器(Go Scheduler)负责将多个Goroutine调度到少量的操作系统线程上执行,并处理它们之间的通信。
2.GO并发编程
2.1 父子协程
在Go语言中,可以通过创建协程(Goroutine)来实现并发执行的任务。当父协程创建一个子协程时,父协程和子协程是相互独立的并发执行单元。父协程可以继续执行其他操作,而不需要等待子协程完成。子协程会在创建后立即开始执行,与父协程并发执行。父协程和子协程之间不存在直接的调用关系,它们是相互独立的执行流程。父协程的结束不会影响子协程的执行。即使父协程结束,子协程仍然可以继续执行,直到完成或被终止。父协程和子协程之间是独立的执行上下文,彼此之间的运行状态不会相互影响。 然而,需要注意的是,如果主协程(即main函数所在的协程)结束了,整个程序会终止,所有的协程也会被强制结束。这意味着如果主协程提前结束,尚未完成的子协程也会被中止。因此,在使用协程进行并发编程时,我们需要确保主协程不会过早地结束,以确保子协程能够完成任务,可以考虑采用以下方法:
- 使用time.Sleep使协程睡眠确保并发子协程完成
- 使用sync.WaitGroup等待组确保并发子协程完成
2.1.1 使用time.Sleep使协程睡眠确保并发子协程完成
time包提供了时间相关的功能,其中最常用的是time.Sleep函数,它可以让当前的Goroutine休眠一段时间。通过结合Goroutine和time.Sleep,我们可以实现协程的并发执行。
package main
import (
"fmt"
"time"
)
func main() {
go task("Task 1") // 启动协程1
go task("Task 2") // 启动协程2
// 主协程休眠一段时间,确保协程有足够的时间执行
time.Sleep(3 * time.Second)
}
func task(name string) {
for i := 0; i < 5; i++ {
fmt.Println(name+":", i) // 打印任务名称和当前迭代值
time.Sleep(500 * time.Millisecond)
}
}
在上面的示例中,我们通过启动两个协程(task1和task2)来实现并发执行。主协程(main函数)休眠3秒钟,确保协程有足够的时间执行。这样,我们就实现了协程的并发执行。
2.1.2 使用sync.WaitGroup等待组确保并发子协程完成
sync.WaitGroup文档介绍:draveness.me/golang/docs… sync包提供了一些同步原语,如WaitGroup等待组,它可以用来等待一组协程的完成。通过WaitGroup,类似于操作系统中的PV信号量,可以实现协程的并发执行和同步等待。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(2) // 设置等待组的计数器为2,表示有两个协程需要等待
go func() {//开启协程1
defer wg.Done() // 协程完成后调用Done方法,减少等待组的计数器
task("Task 1")
}()
go func() {//开启协程1
defer wg.Done() // 协程完成后调用Done方法,减少等待组的计数器
task("Task 2")
}()
wg.Wait() // 等待所有协程完成,完成后才结束main协程
}
func task(name string) {
for i := 0; i < 5; i++ {
fmt.Println(name+":", i) // 打印任务名称和当前迭代值
}
}
在上述示例中,我们使用sync包中的WaitGroup来实现协程的并发执行和同步等待。通过调用wg.Add方法设置等待组的计数器为2,然后在每个协程中使用defer wg.Done()来减少计数器。最后,通过wg.Wait()等待所有协程完成。
2.2 Channel实现并发与协程通信
Channel文档介绍:draveness.me/golang/docs… 在并发编程中,单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义,对共享资源的正确访问需要精确的控制。 在目前的绝大多数语言中,都是通过加锁等线程同步方案来解决这一困难问题,而Go语言却另辟蹊径,它将共享的值通过Channel传递(实际上多个独立执行的线程很少主动共享资源)。channel像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序,可以让一个goroutine发送特定值到另一个goroutine的通信机制。在任意给定的时刻,最好只有一个Goroutine能够拥有该资源,数据竞争从设计层面上就被杜绝了。 这也是Go语言作者提出的并发编程哲学:不要通过共享内存来通信,而应通过通信来共享内存。
虽然我们在 Go 语言中也能使用共享内存加互斥锁进行通信,但是 Go 语言提供了一种不同的并发模型,即通信顺序进程(Communicating sequential processes,CSP)。Goroutine 和 Channel 分别对应 CSP 中的实体和传递信息的媒介,Goroutine 之间会通过 Channel 传递数据。
CSP(Communicating sequential processes)是一种并发编程模型,它强调通过通信来实现并发。在CSP中,程序被分解成一组独立的进程,这些进程通过通道进行通信。通道是一种同步的通机制,它允许进程之间传递数据。CSP模型的一个重要特点是,进程之间的通信是通过发送和收消息来实现的,而不是通过共享内存。 CSP模型的一个优点是,它可以避免一些常见的并发编程问题,例如死锁和竞态条件。这是因为CSP模型中的进程是独立的,们不会相互干扰或阻塞彼此。此外,CSP模型还可以使并发程序更易于理解和调试,因为它们的行为是通过进程之间的通信来定义的。
上图中的两个 Goroutine,一个会向 Channel 中发送数据,另一个会从 Channel 中接收数据,它们两者能够独立运行并不存在直接关联,但是能通过 Channel 间接完成通信。
2.2.1 使用Channel实现协程并发并进行协程通信
package main
import "fmt"
func main() {
ch := make(chan int) // 创建一个int类型的通道
go func() {
ch <- 10 // 发送数据到通道
}()
data := <-ch // 从通道接收数据
fmt.Println(data)
}
在上述示例中,我们通过make函数创建了一个无缓冲的int类型的通道ch,无缓冲的通道只有在有人接收值的时候才能发送值。就。因此,在匿名函数中使用ch <- 10
将数据10发送到通道,就必须还要通过data := <-ch
从通道中接收数据并打印。
2.2.2 使用通道实现生产者消费者模式
生产者消费者模式是并发编程中的常见模式,其中生产者生成数据并将其放入通道,而消费者从通道中取出数据并进行处理。以下是多对多的生产者消费者模式
package main
import (
"fmt"
"sync"
)
// producer 向通道发送数据
func producer(ch chan<- int, id int) {
for i := 0; i < 5; i++ {
ch <- i * id
}
}
// consumer 从通道接收数据
func consumer(ch <-chan int, id int) {
for i := range ch {
fmt.Printf("消费者 %d 接收到数据: %d\n", id, i)
}
}
func main() {
ch := make(chan int, 10)
// wg 用于等待所有协程完成
var wg sync.WaitGroup
// producerWg 用于等待所有生产者协程完成,根据该等待组判断何时关闭通道
var producerWg sync.WaitGroup
// 启动多个生产者协程
for i := 0; i < 3; i++ {
producerWg.Add(1) // 增加生产者等待组计数器
wg.Add(1) // 增加总等待组计数器
go func() {
producer(ch, i+1)
wg.Done() // 减少总等待组计数器
producerWg.Done() // 减少生产者等待组计数器
}()
}
// 启动多个消费者协程
for i := 0; i < 3; i++ {
wg.Add(1) // 增加总等待组计数器
go func() {
consumer(ch, i+1)
wg.Done() // 减少总等待组计数器
}()
}
// 等待所有生产者协程完成后关闭通道
producerWg.Wait()
close(ch)
// 等待所有协程完成
wg.Wait()
}
在上面的示例中,我们创建了一个int类型的通道ch,并将其作为参数传递给生产者和消费者协程。生产者协程通过ch <- i
将数据发送到通道,并打印相关信息。消费者协程使用num := range ch
循环接收通道中的数据,并进行处理。通过关闭通道close(ch)
来通知消费者协程数据已经全部发送完毕。
2.2.3 select多路复用
在某些场景下我们需要同时从多个通道接收数据,Go内置了select关键字,可以同时响应多个通道的操作。
- select可以同时监听一个或多个channel,直到其中一个channel ready
- 如果多个channel同时ready,则随机选择一个执行
- 可以用于判断管道是否存满
package main
import (
"fmt"
"time"
)
func main() {
// 创建两个缓冲区大小为 1 的通道
ch1 := make(chan int, 1)
ch2 make(chan int, 1)
// 向 ch1 发送数据的 goroutine
go func() {
time.Sleep(2 * time.Second)
ch1 <- 1
}()
// 向 ch2 发送数据的 goroutine
go func() {
time.Sleep(1 * time.Second)
ch2 <- 2
}()
// 无限循环,等待从通道接收数据
for {
select {
case x := <-ch1:
fmt.Println("Received from ch1:", x)
return
case x := <-ch2:
fmt.Println("Received from ch2:", x)
return
default:
// 如果两个通道都已满,则打印一条消息并等待 500 毫秒
fmt.Println("All channels are full")
time.Sleep(500 * time.Millisecond)
break
}
}
}
2.3 并发安全性与实现
在并发编程中,要注意并发安全性。并发安全性指的是在并发环境下,多个协程访问共享资源时,能够正确地进行同步和互斥,避免数据竞争和不一致的结果。
在Go语言中,可以通过以下方式实现并发安全:
- 使用互斥锁(Mutex):互斥锁是一种最基本的同步原语,它可以保证同一时刻只有一个 goroutine 可以访问共享资源在访问共享资源之前,需要先获取互斥锁,访问完成后再释放互斥锁。这样可以避免多个 goroutine 同访问共享资源导致的数据竞争问题。
- 使用读写锁(RWMutex):读写锁是一种特殊的互斥锁它可以同时支持多个 goroutine 对共享资源进行读操作,但只能有一个 goroutine 进行写操作。在读写锁中,读操作和写操作是互斥的,但多个读操作间是不互斥的。这样可以提高并发性能,减少锁的竞争。
- 使用原子操作(Atomic):原子操作是一种特殊的操作它可以保证在多个 goroutine 同时访问同一个变量时,对该变量的读写操作是原子的。原子操作可以避免数据竞争问题,但只适用于简单的数据类型,如整数、指针等。
- 使用通道(Channel):通道是一种特殊数据结构,它可以在多个 goroutine 之间传递数据,并且保证传递的数据是并发安全的。在使用通道时需要注意通道的缓冲区大小和通道的方向,以避免死锁和数据竞争问题。
- 使用 sync 包中的其他同步原语:Go语言的标准库中提供了许多同步原语,如条件变(Cond)、信号量(Semaphore)等,可以根据具体的需求选择合适的同步原语来实现并发安全。
2.3.1 使用互斥锁(Mutex)
Mutex文档介绍:draveness.me/golang/docs…
package main
import (
"fmt"
"sync"
)
var x int64
var wg sync.WaitGroup
var lock sync.Mutex
func add() {
for i := 0; i < 5000; i++ {
lock.Lock() // 加锁
x = x + 1
lock.Unlock() // 解锁
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
2.3.2 使用读写锁(RWMutex)
RWMutex文档介绍:draveness.me/golang/docs… 读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。
package main
import (
"fmt"
"sync"
)
var (
x int64
wg sync.WaitGroup
lock sync.Mutex
rwlock sync.RWMutex
)
func write() {
rwlock.Lock() // 加写锁
x = x + 1
time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
rwlock.Unlock() // 解写锁
wg.Done()
}
func read() {
rwlock.RLock() // 加读锁
time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
rwlock.RUnlock() // 解读锁
wg.Done()
}
func main() {
start := time.Now()
for i := 0; i < 10; i++ {
wg.Add(1)
go write()
}
for i := 0; i < 1000; i++ {
wg.Add(1)
go read()
}
wg.Wait()
end := time.Now()
fmt.Println(end.Sub(start))
}
2.3.3 使用原子操作(Atomic)
package main
import (
"fmt"
"sync"
"sync/atomic"
)
var counter int64 // 定义一个int64类型的计数器变量
func main() {
var wg sync.WaitGroup
// 启动10个goroutine,每个goroutine调用atomic.AddInt64方法自增计数器
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt64(&counter, 1)
}()
}
wg.Wait()
fmt.Println("Counter value:", counter)
}
2.3.4 使用通道(Channel)定义goroutine 池限制并发度
package main
import (
"fmt"
"time"
)
// 定义go协程池结构体
type GoPool struct {
MaxLimit int // 最大并发度
tokenChan chan struct{} // 控制并发执行的令牌通道
}
func main() {
gopool := NewGoPool(3) // 最大并发度为 3
defer gopool.Wait()
//开启10个线程,但最大并发度为3
for i := 0; i < 10; i++ {
taskID := i
gopool.Submit(func() {
// 模拟任务处理逻辑
fmt.Printf("任务 %d 开始执行\n", taskID)
time.Sleep(time.Second)
fmt.Printf("任务 %d 完成\n", taskID)
})
}
}
// NewGoPool 创建一个 GoPool 对象,max 设置最大并行度
func NewGoPool(max int) *GoPool {
p := &GoPool{}
p.MaxLimit = max
p.tokenChan = make(chan struct{}, p.MaxLimit)
for i := 0; i < p.MaxLimit; i++ {
p.tokenChan <- struct{}{} // 初始化 tokenChan
}
return p
}
// Submit 提交任务
// 每个任务都会异步执行,并且会在执行完后释放一个令牌
func (gp *GoPool) Submit(fn func()) {
token := <-gp.tokenChan // 如果没有可用令牌,则会阻塞
go func() {
fn()
gp.tokenChan <- token // 执行完成后释放令牌,以便其他任务可以获得执行机会
}()
}
// Wait 等待所有任务执行完成
func (gp *GoPool) Wait() {
for i := 0; i < gp.MaxLimit; i++ {
<-gp.tokenChan // 等待所有令牌被释放
}
close(gp.tokenChan) // 关闭令牌通道,释放资源
}
func (gp *GoPool) Size() int {
return len(gp.tokenChan) // 返回当前令牌通道中的可用令牌数量
}
这段代码创建了一个名为 "GoPool" 的结构体,它实现了一个简易的 goroutine 池来限制并发度。结构体包含最大并发度和控制同一时间能够执行 goroutine 数量的令牌通道。以下是函数及其功能:
- NewGoPool(max int) *GoPool:创建一个新的 GoPool 对象,max 用于设置最大并行度。该函数返回了 GoPool 结构体的指针。
- Submit(fn func()):向 goroutine 池的任务队列提交一个新的任务,fn 代表但不限于该任务函数。每个任务都会异步执行,并且在执行完成后释放一个令牌。
- Wait():等待所有提交的任务执行完成。 这等待所有令牌被释放以及 TaskQueue 中的所有任务被处理。
- Size():返回当前令牌通道中的可用令牌数量。
main 函数演示了如何使用 GoPool 来异步运行一组任务。具体而言,使用 for 循环提交了十个匿名函数。 这些函数模拟处理逻辑,并在开始和结束时打印与任务 ID 相关的消息。在这种情况下,goroutine 池的最大并行度设置为3,这意味着在任何给定时间,只能有三个任务同时运行。
2.3.5 使用 sync 包中的其他同步原语(以信号量semaphore为例)
semaphore文档介绍:draveness.me/golang/docs…
package main
import (
"fmt"
"olang.org/x/sync/semaphore"
"sync"
)
var sem *semaphore.Weighted = semaphore.NewWeighted(1) // 创建一个权重为1的信号量
func main() {
var wg sync.WaitGroup
// 启动10个goroutine,每个goroutine获取信号量并输出信息
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
sem.Acquire(nil, 1) // 获取信号量
defer sem.Release(1) // 释放信号量
fmt.Println("Goroutine", id, "is running")
}(i)
}
wg.Wait()
}
2.4 recovery恢复程序运行
在 Go 语言中,recovery 是一种机制,用于在程序发生 panic 时恢复程序的执行。当程序发生 panic 时,recovery 可以获 panic,并在程序崩溃前进行一些处理,例如输出日志、释放资源等。 在Go语言中,可以使用panic()
函数来抛出一个异常,从而在协程中添加错误。例如,我们可以在子协程中添加一个除数为0的错误,如下所示:
package main
import (
"fmt"
"time"
)
func childRoutine() {
defer func() {
if r := recover(); r != nil {
// 处理子协程中的错误
fmt.Println("子协程出现错误:", r)
}
}()
// 子协程中的代码
a, b := 10, 0
c := a / b // 除数为0,会抛出异常
fmt.Println("子协程执行完毕", c)
}
func main() {
// 启动子协程
go childRoutine()
// 主协程中的代码
time.Sleep(time.Millisecond * )
fmt.Println("主协程执行完毕")
}
在这个示例中,我们在子协程中定义了两个变量a
和b
,并将b
的值设置为0然后,我们尝试将a
除以b
,这会抛出一个异常。在defer
语句中,我们使用recover()
函数捕获这个异常,并在控制台输出错误信息。
当我们运行这个程序时,子协程会抛出一个异常,但是由于我们使用了recover()
函数来捕获异常,程序不会崩溃,而是会在控制台输出错误信息,并继续执行其他协程。这样,我们就可以在协程中添加错误,避免程序崩溃,更好地管理协程并处理异常,确保程序的稳定性。
3.Context上下文
Context文档介绍:draveness.me/golang/docs…
在 Goroutine 构成的树形结构中对信号进行同步以减少计算资源的浪费是 context.Context 的最大作用。Go 服务的每一个请求都是通过单独的 Goroutine 处理的,HTTP/RPC 请求的处理器会启动新的 Goroutine 访问数据库和其他服务。
如下图所示,我们可能会创建多个 Goroutine 来处理一次请求,而 context.Context 的作用是在不同 Goroutine 之间同步请求特定数据、取消信号以及处理请求的截止日期。
- context 主要用于控制协程的生命周期和传递上下文信息。通过 context,我们可以在协程之间传递一些额外的信息,例如请求 ID、超时时间、取消信号等。同时,context 还提供了一种机制来取消或超时协程,以避免因为某个协程长时间阻塞而导致整个程序变得不可用。
- channel 主要用于协程的通信和同步。通过 channel,我们可以在协程之间传递数据和信号,例如任务、消息、事件等。同时, 还提供了一种机制来同步协程,以避免因为协程之间的竞争而导致数据不一致或死锁等问题。
以下是 context 在 Go 并发编程中的一些应用:
- 传递元数据:context 可以用于在多个 Goroutine 之间传递请求范围的元数据,例如请求 ID、认证令牌等。
- 取消操作:context 可以用于在多个 Goroutine 之间传递取消信号。当一个操作需要被取消时,可以使用 context 通知所有相关的 Goroutine 停止工作。
- 超时控制:context 可以用于设置操作的超时时间。当操作超过指定的时间限制时,context 会自动发送取消信号,使得相关的 Goroutine 可以及时停止工作。
以下是实现了这三种功能的简单代码示例
package main
import (
"context"
"fmt"
"time"
)
type keyType string
const key keyType = "value"
// worker 是一个 Goroutine,它会不断检查 context 中的元数据值
// 当元数据值达到 5 或超过 2 秒时,Goroutine 将被取消。
func worker(ctx context.Context) {
select {
case <-ctx.Done():
fmt.Println("已超时,结束协程")
return
default:
id := ctx.Value(key).(int) // 从 context 中获取数据值
if id >= 5 {
fmt.Println("id超过5,结束协程")
return
}
fmt.Printf("接收到查询id为%d\n", id)
time.Sleep(500 * time.Millisecond)
}
}
func main() {
// 创建一个带有超时功能的 context,超时时间为 2 秒
//context.Background() 作为根 context,返回回一个空的 context
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
// 使用 context.WithValue 为 context 添加一个名为 value 的元数据,初始值为 0
ctx = context.WithValue(ctx, key, 0)
// 逐渐增加 context 中的元数据值,并在循环结束后等待 3 秒,以确保 Goroutine 有足够的执行。
for i := 1; i <= 10; i++ {
time.Sleep(300 * time.Millisecond)
ctx = context.WithValue(ctx, key, i) // 更新 context 中的元数据值
// 启动 worker Goroutine
go worker(ctx)
}
time.Sleep(3 * time.Second)
}
4. GMP调度模型
GMP模型是一个高效的并行计算模型,它是Go语言运行时系统的一部分,负责将Go程序中的goroutine分配给多个处理器,以实现并行计算,从而提高程序的性能。 GMP调度模型支持任务窃取(task stealing)机制。当一个处理器空闲时,它可以从其他处理器的工作队列中窃取一个gor进行处理。这种机制可以使得goroutine的负载更加均衡,从而提高程序的性能。 GMP调度模型支持动态调整处理器数量的功能。当程序的负载发生变化时,调度器可以动态地增加或减少处理器的数量,以适应程序的需求。这种机制可以使得计算资源的利用更加高效。
GMP介绍文档:www.topgoer.cn/docs/golang…
4.1GMP 模型介绍
M(machine)
- M代表着真正的执行计算资源,可以认为它就是os thread(系统线程)。
- M是真正调度系统的执行者,每个M就像一个勤劳的工作者,总是从各种队列中找到可运行的G,而且这样M的可以同时存在多个。
- M在绑定有效的P后,进入调度循环,而且M并不保留G状态,这是G可以跨M调度的基础。
P(processor)
- P表示逻辑processor,是线程M的执行的上下文。
- P的最大作用是其拥有的各种G对象队列、链表、cache和状态。
G(goroutine)
- 调度系统的最基本单位goroutine,存储了goroutine的执行stack信息、goroutine状态以及goroutine的任务函数等。
- 在G的眼中只有P,P就是运行G的“CPU”。
- 相当于两级线程
- 全局队列(Global Queue):存放等待运行的 G。
- P 的本地队列:同全局队列类似,存放的也是等待运行的 G,存的数量有限,不超过 256 个。新建 G’时,G’优先加入到 P 的本地队列,如果队列满了,则会把本地队列中一半的 G 移动到全局队列。
- P 列表:所有的 P 都在程序启动时创建,并保存在数组中,最多有 GOMAXPROCS(可配置) 个。
- M:线程想运行任务就得获取 P,从 P 的本地队列获取 G,P 队列为空时,M 也会尝试从全局队列拿一批 G 放到 P 的本地队列,或从其他 P 的本地队列偷一半放到自己 P 的本地队列。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。
4.2调度器的设计策略
- 复用线程:避免频繁的创建、销毁线程,而是对线程的复用。
- work stealing 机制:当本线程无可用的G时,先从全局队列中取,如果全局队列中也没有的话,就取其他线程绑定的P偷取P,而不是销毁线程
- hand off 机制: 当本线程因为 G 进行系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的线程执行。
- 利用并行:GOMAXPROCS 设置 P 的数量,最多有 GOMAXPROCS 个线程分布在多个 CPU 上同时运行。GOMAXPROCS 也限制了并发的程度,比如 GOMAXPROCS = 核数/2,则最多利用了一半的 CPU 核进行并行。
- 抢占:在 coroutine 中要等待一个协程主动让出 CPU 才执行下一个协程,在 Go 中,一个 goroutine 最多占用 CPU 10ms,防止其他 goroutine 被饿死,这就是 goroutine 不同于 coroutine 的一个地方。
- 全局 G 队列:在新的调度器中依然有全局 G 队列,当绑定的P中没有G可以执行的时候,就去全局G队列中找
4.3 go func () 调度流程
- 我们通过 go func () 来创建一个 goroutine;
- 有两个存储 G 的队列,一个是局部调度器 P 的本地队列、一个是全局 G 队列。新创建的 G 会先保存在 P 的本地队列中,如果 P 的本地队列已经满了就会保存在全局的队列中;
- G 只能运行在 M 中,一个 M 必须持有一个 P,M 与 P 是 1:1 的关系。M 会从 P 的本地队列弹出一个可执行状态的 G 来执行,如果 P 的本地队列为空,则首先从全局获取,若全局也为空就会向其他的 MP 组合偷取一个可执行的 G 来执行;
- 一个 M 调度 G 执行的过程是一个循环机制;
- 当 M 执行某一个 G 时候如果发生了 syscall 或则其余阻塞操作,M 会阻塞,如果当前有一些 G 在执行,runtime 会把这个线程 M 从 P 中摘除 (detach),然后再创建一个新的操作系统的线程 (如果有空闲的线程可用就复用空闲线程) 来服务于这个 P;
- 当 M 系统调用结束时候,这个 G 会尝试获取一个空闲的 P 执行,并放入到这个 P 的本地队列。如果获取不到 P,那么这个线程 M 变成休眠状态, 加入到空闲线程中,然后这个 G 会被放入全局队列中。
4.4调度器的生命周期
- M0:m0就是进程启动后的初始线程
- G0:代表着初始线程的stack
链接:https://juejin.cn/post/7265939798794764322
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。