go协程入门


| 阅读 |,阅读约 4 分钟
| 复制链接:

Overview

协程在手,说Go就Go

协程概述

  • 协程:轻量级、用户级线程。
  • 协程的调度:
    • 由用户程序进行协作式调度,不像内核进程、线程一样是抢占式调度
  • 协程的优势:
    • 占用空间少(只需2k,系统线程为2M)
    • 线程上下文切换成本少
  • go:语言层面提出了Groutine的概念,支持协程

简单入门

  • go语言中通过go关键字来调用
 1package main
 2
 3import (
 4  "fmt"
 5  "time"
 6)
 7
 8func One(){
 9  fmt.Println("1")
10}
11
12func Two(){
13  fmt.Println("2")
14}
15
16func main(){
17  go One()
18  go Two()
19}

上面的代码执行后,并不会如期打印结果,原因在于协程是并发的,协程调用前,主函数已经退出,协程也被销毁了。

1func main(){
2  go gorouting()
3  // 可通过简单的sleep,让主线程等待协程执行完
4  // 但是执行顺序不一定是按照1,2顺序输出
5  time.Sleep(5 * 1e9)
6}
7

前面的例子,可看到协程使用需要考虑:

  • 如何控制协程调用顺序(特别是访问临界资源)
  • 如何实现不同协程的并发通讯

实现思路:

  • 同步问题:sync同步锁
  • 通讯问题:channel

sync同步锁

go中sync包提供了2个锁,互斥锁sync.Mutex和读写锁sync.RWMutex.我们用互斥锁来解决上述的不同的协程可能同时调度同一个资源的问题

channel

概述

  • channel是go语言中一种特殊的数据类型
  • 可以通过chanel发送类型化数据,实现协程通讯
  • channel是有方向的,包括流入和流出

基本使用

1// 普通channel
2var ch chan string
3ch = make(chan string)
4// 只写chanel(流入)
5var writeCh chan <- int
6// 只读chanel(流出)
7var readCh <- chan int

上述chanel特点

上述channel不带缓冲区,或者说长度为1,有如下特点:

  • 同一时间只有一条数据
  • 一旦有数据放入,必须被取出才能继续放入

channel控制执行顺序

  • 下面的代码可以保证先执行One,再执行Two
 1package main
 2
 3import (
 4  "fmt"
 5  "time"
 6)
 7
 8func One(ch chan int){
 9  fmt.Println("1")
10  ch <- 1
11}
12
13func Two(ch chan int){
14  <- ch
15  fmt.Println("2")
16}
17
18func main(){
19  ch := make(chan int)
20  go One(ch)
21  go Two(ch)
22  time.Sleep(5 * 1e9)
23}

将main函数本身也看成协程,不用sleep实现同步

 1package main
 2
 3import (
 4  "fmt"
 5)
 6
 7func One(ch chan int){
 8  fmt.Println("1")
 9  ch <- 1
10}
11
12func Two(ch chan int){
13  <- ch
14  fmt.Println("2")
15}
16
17func main(){
18  ch := make(chan int)
19  go One(ch)
20  <- ch
21  //go Two(ch)
22  //time.Sleep(5 * 1e9)
23}

range

range用于列表的元素遍历,不仅仅可以遍历普通元素,还可以遍历channel

  • 持续的访问数据源并检查channel是否已经关闭,并不高效。go中提供了range关键字。
  • range关键字在使用channel的时候,会自动等待channel的动作一直到channel关闭。通俗点将就是channel可以自动开关。

带缓存的channel

申明channel时,可以指定长度,长度不为1的channel,可以称为带缓存的channel,数据可以连续写入,直到占满长度为止

1ch = make(chan int, 3)
2ch <- 1
3ch <- 2
4ch <- 3

go携程实现生产者-消费者模型

下面的例子是结合协程和range实现的生产者-消费者模型

 1package main
 2import "fmt"
 3
 4var count = 0
 5
 6func main() {
 7  exitChan := make(chan int, 5)
 8  c := make(chan int, 50)
 9
10  // 这里任务数刚好是channel的大小,不会报错
11  // 如果改成比chanel大,就会报错。但是如果把这段代码放到consumer后面,就不会报错
12  for i := 0; i < 50; i++ {
13    c <- i
14  }
15
16  for i := 0; i < 5; i++ {
17    go Consumer(i, c, exitChan)
18  }
19  // 这里关闭任务channel,让调用Consumer函数的协程知道:自己的任务完成了,协程可以退出了
20  // 如果不关闭,协程将不退出
21  close(c)
22  for i := 0; i < 5; i++ {
23    <-exitChan
24  }
25  close(exitChan)
26  fmt.Println(count)
27}
28func Consumer(index int, c chan int, exitChan chan int) {
29  for target := range c {
30    fmt.Printf("no.%d:%d\n", index, target)
31    count++
32  }
33  // 执行close(c)后,协程会走到这里
34  exitChan <- index
35}

select

在UNIX中,select()函数用来监控一组描述符,该机制常被用于实现高并发的socket服务器程序。Go语言直接在语言级别支持select关键字,用于处理异步IO问题。

首先要明确select做了什么??

  • select可以监听多个channel相关的io操作,当io发送时,触发相应操作
  • select中存在着一种轮询机制,select监听进入通道的数据,也可以是通道发送值的时候,监听到相应的行为后就执行case里面的操作
  • select默认是阻塞的,只有当监听的channel中有发送或接收可以进行时才会运行,当多个channel都准备好的时候,select是随机的选择一个执行的。
 1// 一个实现超时的例子
 2timeout := make(chan bool, 1)
 3
 4go func() {
 5    time.Sleep(1e9)
 6    timeout <- true
 7}()
 8
 9switch {
10    case <- ch:
11    // 从ch中读取到数据
12
13    case <- timeout:
14    // 没有从ch中读取到数据,但从timeout中读取到了数据
15}

协程调度

go中的runtime包,提供了调度器的功能,runtime包提供了以下几个方法:

Gosched:让当前线程让出 cpu 以让其它线程运行,它不会挂起当前线程,因此当前线程未来会继续执行 NumCPU:返回当前系统的 CPU 核数量 GOMAXPROCS:设置最大的可同时使用的 CPU 核数 Goexit:退出当前 goroutine(但是defer语句会照常执行) NumGoroutine:返回正在执行和排队的任务总数 GOOS:目标操作系统