这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天
内容源于青训营课堂视频以及一些go文档和自己的经验、理解,若有错误欢迎及时指出
1.1 Goroutine
goroutine机制类似于线程,但是在运行时调度和管理的,程序会智能地将goroutine中的任务合理分配给CPU,在语言层面内置了调度和上下文切换的机制
协程:用户态,轻量级线程,栈MB级
线程:内核态,线程跑多个协程,栈KB级
使用goroutine
启动协程使用go关键字即可,一个goroutine一定对应一个函数,一个函数可以由多个goroutine执行
func main() {
for i := 0; i < 100; i++ {
go func(i int) {
fmt.Println(i) //此时用的是函数外面的i
}(i) //闭包函数的话
}
fmt.Println("main")
time.Sleep(time.Second)
}
可增长的栈
OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,他可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这么大。所以在Go语言中一次创建十万左右的goroutine也是可以的。
goroutine调度机制
GPM是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。
- G goroutine 每个go关键字都会创建一个协程,里面有与所在P的绑定等信息
- P processor 管理着一组G队列,并进行调度(比如把占用CPU时间较长的G暂停、运行后续的G等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。
- M machine 是GO运行时对操作系统内核线程的虚拟,M与内核线程是一一映射的关系,G都要放到M上才能运行
单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。
1.2CSP
Communicating Sequential Processes
Go语言提倡通过通信共享内存而不是通过共享内存而实现通信
- 通过通信共享内存
- 使用
channel实现,类似消息队列
- 使用
- 共享内存实现通信
- 临界区加锁,影响性能
1.3Channel
make关键字
- 无缓冲(同步)
make (chan int) - 有缓冲
make (chan int, 2)
通道的操作
箭头代表的是数据流向
-
发送值
ch1 <- 1 -
接受值
x := <- ch1 给x赋值
<- ch1 直接丢掉了 -
关闭
close(ch1)关闭一个未初始化的或已经关闭的channel会引发panic
1.4并发安全Lock
- mutex互斥锁
var x = 0
var wg sync.WaitGroup
var lock sync.Mutex
func add() {
lock.Lock()
for i := 0; i < 500000; i++ {
//lock.Lock()
x = x + 1
//lock.Unlock()
}
lock.Unlock()
wg.Done()
}
func main() {
wg.Add(3)
go add()
go add()
go add()
//使用了公共空间导致同时写入的时候部分丢失了,少于150000
//协程越多,重复写入可能性越大,差距越大
wg.Wait()
fmt.Println(x)
}
使用了公共空间导致同时写入的时候部分丢失了,少于150000 协程越多,重复写入可能性越大,差距越大
- rwLock 读写互斥锁 当读的操作数量远大于写操作的时候,读写互斥锁效率高 读的时候别人不需要等待,只有写的时候要等
var (
x = 0
//lock sync.Mutex
wg sync.WaitGroup
rwLock sync.RWMutex
)
1.5WaitGroup
var wg sync.WaitGroup
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) //计数
go f1(i)
} //如何知道这10个goroutine都执行结束了?
wg.Wait() //等待wg的计数器减为0
}