最近用golang做项目时,遇到了协程并发控制的问题,所以详细学习一下协程部分的内容,从以下几个点去学习:
什么是协程
协程的底层实现
协程的并发控制
一、什么是协程
记得在学习操作系统中进程和线程的时候,通过进程和线程的对比来学习二者的区别和联系,所以不妨先学习进程、线程和协程之间的区别和联系:
-
线程和进程都是由OS来进行调度,有CPU时间片的概念和多种调度算法
-
协程也叫做用户级线程,也就是对内核是透明的,完全由用户来控制,不受OS调度,所以也就没有相应的调度算法来悄悄的进行协程之间的切换
我觉得上面的对比可以很好的理解协程和它的优势,以前用java写程序的时候,总会做一个线程池来控制并发,为的是防止系统开销过大,提高系统效率,因为线程是OS层面的,它的调度完全依赖于OS,每次做线程的切换都需要OS从用户态转换到内核态去做操作,效率太低。而golang就是为并发而生,用户级线程也就意味着协程不需要OS来做上下文切换
在golang中goroutine本质上就是协程,详细的说,goroutine可以理解成 Golang在runtime、调用等多个方面做了封装和处理的协程,当遇到长时间执行或者进行系统调用时,会主动把当前 goroutine 的CPU (P) 转让出去,让其他 goroutine 能被调度并执行,也就是 Golang 从语言层面支持了协程。Golang 的一大特色就是从语言层面原生支持协程,在函数或者方法前面加 go关键字就可创建一个协程
关于goroutine的调度器实现机制可以查一下GPM模型
协程的底层实现
他和线程的原理是一样的,当 a线程 切换到 b线程 的时候,需要将 a线程 的相关执行进度压入栈,然后将 b线程 的执行进度出栈,进入 b线程 的执行序列。协程只不过是在 应用层 实现这一点。但是,协程并不是由操作系统调度的,而且应用程序也没有能力和权限执行 cpu 调度。怎么解决这个问题?
答案是,协程是基于线程的。内部实现上,维护了一组数据结构和 n 个线程,真正的执行还是线程,协程执行的代码被扔进一个待执行队列中,由这 n 个线程从队列中拉出来执行。这就解决了协程的执行问题。
那么协程是怎么切换的呢?答案是:golang 对各种 io函数 进行了封装,这些封装的函数提供给应用程序使用,而其内部调用了操作系统的异步 io函数,当这些异步函数返回 busy 或 bloking 时,golang 利用这个时机将现有的执行序列压栈,让线程去拉另外一个协程的代码来执行,基本原理就是这样,利用并封装了操作系统的异步函数。包括 linux 的 epoll、select 和 windows 的 iocp、event 等。
由于golang是从编译器和语言基础库多个层面对协程做了实现,所以,golang的协程是目前各类有协程概念的语言中实现的最完整和成熟的。十万个协程同时运行也毫无压力。关键我们不会这么写代码。但是总体而言,程序员可以在编写 golang 代码的时候,可以更多的关注业务逻辑的实现,更少的在这些关键的基础构件上耗费太多精力。
协程的并发控制
如果我们想要执行的协程中有比如访问数据库的操作的时候,我们就必须得控制协程的并发量,不然很可能就会让数十万个请求落到数据库上导致崩溃,有一种控制并发的方式是通过channel
先来看一下使用的例子:
假设我们要从一个存有10万个uin的切片中读取uin并查询每个uin对应的用户信息
for _,v := range Uins{
//对每个uin查询对应的用户信息
go getUserInfo(v)
}
如果这样去查的话很可能将数据库打崩掉,我们引入channel
//创建一个大小为512的空结构体channel
channel := make(chan struct{},512)
for_,v:= range Uins{
channel <- struct{}{}
go getUserInfo(v,channel)
}
func getUserInfo(v int,channel chan struct{}){
defer func() {
<-channel
}()
/**
业务逻辑
**/
}
这样便可以控制协程的并发量,当cahnnel里的空结构体满了之后,程序便会阻塞,等待消耗完后继续执行
但是这样做可能导致最后一组协程没有办法执行完,因为一旦通道内剩余的容量大于剩余的任务数时,就不会有阻塞,main函数就会直接结束,为了保证所有的协程都执行完毕,我们需要引入sync.WaitGroup来控制
func main(){
//创建一个大小为512的空结构体channel
channel := make(chan struct{},512)
//创建sync.WaitGroup
var wg sync.WaitGroup
//wg的值为Uins的长度
wg.Add(len(Uins))
for_,v:= range Uins{
channel <- struct{}{}
//把wg当作参数传入协程
go getUserInfo(v,channel,&wg)
}
//一直阻塞直到wg的值为0
wg.Wait()
return
}
func getUserInfo(v int,channel chan struct{},wg *sync.WaitGroup){
defer func() {
//return前 wg的值减1
wg.Done()
<-channel
}()
/**
业务逻辑
**/
}