Go的GMP调度模型是其并发实现的核心。
调度流程可以概括为:
每个P会维护一个可运行的G队列 (Local Run Queue, LRQ)。
M需要获取一个P才能执行G。M会从其绑定的P的LRQ中获取G并执行。
如果P的LRQ为空,M会尝试从全局G队列 (Global Run Queue, GRQ)或其他P的LRQ中窃取 (Work Stealing) G来执行,以实现负载均衡。
当G执行系统调用或发生阻塞时,它所占用的M会和P解绑(Hand Off),P会去寻找其他空闲的M,或者创建一个新的M来继续执行队列中的其他G。阻塞的G结束后会尝试重新进入某个P的队列等待执行。
通过这种方式,GMP模型实现了高效的用户态调度,避免了频繁的内核态线程切换,并能充分利用多核CPU资源。
Go语言的垃圾回收 (GC) 主要是为了自动管理内存,减轻开发者的负担,避免内存泄漏。
Go的GC采用的是并发的三色标记清除 (Concurrent Tri-color Mark-and-Sweep) 算法。
核心思想可以概括为:
GC 开始将栈上的可达对象全部扫描并标记为黑色 (之后不再进行第二次重复扫描,无需 STW),
GC 期间,任何在栈上创建的新对象,均为黑色。(混合写屏障加入了这两条对栈区的操作,使得不需要STW)
被删除的对象标记为灰色。(删除写屏障)
被添加的对象标记为灰色。(插入写屏障)
补充: 插入写屏障:只应用于堆区,如果一个对象在并发过程中创建了一个新的对象,那么新的对象直接标记为灰色。
删除写屏障:如果一个对象在并发过程中被删除,那么直接标记该删除的对象为灰色。
实现闭包的一个重要前提就是函数是一等公民,它可以向其它类型的参数一样被传递。如果我们有一个函数,并且在这个函数里面又定义了另一个函数,且这个内部函数可以使用外部函数里面定义的变量,这就是闭包。当我们声明一个匿名函数时,他天生就是一个闭包。
函数是一等公民,意味着可以把函数赋值给变量或存储在数据结构中,也可以把函数作为其它函数的参数或者返回值。
Mutex 也称为互斥锁,互斥锁就是互相排斥的锁,它可以用作保护临界区的共享资源,保证同一时刻只有一个 goroutine 操作临界区中的共享资源。互斥锁 Mutex类型有两个方法,Lock和 Unlock。
type Mutex struct {
state int32 // 互斥锁的状态
sema uint32 // 信号量,用于控制互斥锁的状态
}
使用互斥锁的注意事项
RWMutex 也称为读写互斥锁,读写互斥锁就是读取/写入互相排斥的锁。它可以由任意数量的读取操作的 goroutine 或单个写入操作的 goroutine 持有。读写互斥锁 RWMutex 类型有五个方法,Lock,Unlock,Rlock,RUnlock 和 RLocker。其中,RLocker 返回一个 Locker 接口,该接口通过调用rw.RLock 和 rw.RUnlock 来实现 Lock 和 Unlock 方法。
type RWMutex struct {
w Mutex // held if there are pending writers
writerSem uint32 // semaphore for writers to wait for completing readers
readerSem uint32 // semaphore for readers to wait for completing writers
readerCount atomic.Int32 // number of pending readers
readerWait atomic.Int32 // number of departing readers
}
使用读写互斥锁的注意事项
Mutex 和 RWMutex 的区别
sync.WaitGroup 用于阻塞等待一组 Go 程的结束。如果有一个任务可以分解成多个子任务进行处理,同时每个子任务没有先后执行顺序的限制,等到全部子任务执行完毕后,再进行下一步处理。这时每个子任务的执行可以并发处理,这种情景下适合使用sync.WaitGroup。
标准用法
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(3)
go handlerTask1(&wg)
go handlerTask2(&wg)
go handlerTask3(&wg)
wg.Wait()
fmt.Println("全部任务执行完毕.")
}
func handlerTask1(wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println("执行任务 1")
}
func handlerTask2(wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println("执行任务 2")
}
func handlerTask3(wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println("执行任务 3")
}
sync.Cond 条件变量用来协调想要访问共享资源的那些 goroutine,当共享资源的状态发生变化的时候,它可以用来通知被互斥锁阻塞的 goroutine。假设一个场景:有一个协程在异步地接收数据,剩下的多个协程必须等待这个协程接收完数据,才能读取到正确的数据。在这种情况下,如果单纯使用 chan 或互斥锁,那么只能有一个协程可以等待,并读取到数据,没办法通知其他的协程也读取数据。这个时候,就需要有个全局的变量来标志第一个协程数据是否接受完毕,剩下的协程,反复检查该变量的值,直到满足要求。或者创建多个 channel,每个协程阻塞在一个 channel 上,由接收数据的协程在数据接收完毕后,逐个通知。总之,需要额外的复杂度来完成这件事。Go 语言在标准库 sync 中内置一个sync.Cond 用来解决这类问题。和sync.Cond相关的有4个方法:
package main
import (
"log"
"sync"
"time"
)
var done bool
func main() {
// 1. 定义一个互斥锁,用于保护共享数据
mu := sync.Mutex{}
// 2. 创建一个sync.Cond对象,关联这个互斥锁
cond := sync.NewCond(&mu)
go read("reader1", cond)
go read("reader2", cond)
go read("reader3", cond)
write("writer", cond)
time.Sleep(time.Second * 3)
}
func read(name string, c *sync.Cond) {
// 3. 在需要等待条件变量的地方,获取这个互斥锁,并使用Wait方法等待条件变量被通知;
c.L.Lock()
for !done {
c.Wait()
}
log.Println(name, "starts reading")
c.L.Unlock()
}
func write(name string, c *sync.Cond) {
// 4. 在需要通知等待的协程时,使用Signal或Broadcast方法通知等待的协程。
log.Println(name, "starts writing")
time.Sleep(time.Second)
c.L.Lock()
done = true
c.L.Unlock()
log.Println(name, "wakes all")
c.Broadcast() // 如果不广播, read()方法的 log.Println(name, "starts reading")不会执行
}
/*
输出:
2024/10/02 21:27:50 writer starts writing
2024/10/02 21:27:51 writer wakes all
2024/10/02 21:27:51 reader3 starts reading
2024/10/02 21:27:51 reader1 starts reading
2024/10/02 21:27:51 reader2 starts reading
*/
sync.pool用于保存和复用临时对象,减少内存分配,降低 GC 压力。使用方式:
package main
import (
"encoding/json"
"fmt"
"sync"
)
type Student struct {
Name string `json:"name"`
}
var studentPool = sync.Pool{
New: func() interface{} {
return new(Student)
},
}
func main() {
buf := `{"name":"Mike"}`
stu := studentPool.Get().(*Student)
fmt.Println(*stu) // {}
err := json.Unmarshal([]byte(buf), stu)
if err != nil {
fmt.Printf("err:%s,err")
return
}
fmt.Println(*stu)
studentPool.Put(stu) // {Mike}
stu2 := studentPool.Get().(*Student)
fmt.Println(*stu2) // {Mike}
}
在Go语言中,panic和recover构成了处理程序运行时错误的两个基本机制。它们用于在出现严重错误时,能够优雅地终止程序或恢复程序的执行。
panic通常用于处理那些无法恢复的错误,比如空指针引用、数组越界等。这些错误如果不加以处理,将会导致程序崩溃。
需要注意的是,recover只有在defer函数中直接调用时才有效。在其他地方调用recover是无效的,它将返回nil并且不会终止panic。
在Go语言中,goroutine(Go routine)是一种轻量级的执行单元。可以将其理解为一个函数的并发执行,类似于线程,但比线程更轻量级。与传统的线程相比,创建和销毁goroutine的开销非常小。在Go程序中,可以轻松地创建成千上万个goroutine,每个goroutine都能够独立执行,而不需要手动管理线程和锁。这使得在Go语言中实现并发变得非常容易。要创建一个goroutine,只需要在函数调用前加上关键字"go"即可。Go语言的运行时系统(runtime)负责调度和管理goroutine的执行。运行时系统在多个逻辑处理器上调度goroutine,使得它们可以并发执行。以下是goroutine的底层实现原理的一些关键点: