Golang协程并发控制

1,596 阅读5分钟

最近用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
	}()
    /**
    业务逻辑  
    **/
}