(八)Goroutine 笔记

支持高并发编程模式是 Go 的一大特色。其中,Goroutine(协程)是 Go 中最基本的执行单元。每个 Go 程序至少包含一个 主 Goroutine。

并发与并行区别:

  • 并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
    在这里插入图片描述

  • 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。

在这里插入图片描述

协程 Goroutine

Go 语言支持并发,通过 go 关键字来开启 goroutine 。goroutine 是轻量级线程,调度由 Golang 运行时进行管理的。

  • 轻量级线程
  • 非抢占式多任务处理,由协程主动交出控制权
  • 编译器/解释器/虚拟机层面的多任务
  • 多个协程可能再一个或多个线程上运行
func main() {
    for i := 0; i < 1000; i++ {
        // 使用 go 语句开启一个新的运行期线程, 即 goroutine
        // 通过新创建的 goroutine 来执行一个函数。 
        // 同一个程序中的所有 goroutine 共享同一个地址空间。
        go func(i int) {
            for {
                fmt.Printf("Hello from goroutine %d\n", i)
            }
        }(i)
    }
    // 延迟程序退出以打印出 go函数执行结果
    time.Sleep(time.Millisecond)
}

协程交出控制权:

  • IO 调用
  • 使用 runtime.Gosched() 方法

go run -race go文件 检测数据访问冲突

主 goroutine

package main
import "fmt"
func main() {
	for i := 0; i < 10; i++ {
		go func() {
			fmt.Println(i)
		}()
	}
}
// 不会打印任何内容

每一个独立的 Go 程序在运行时也总会有一个主 goroutine。这个主 goroutine 会被自动地启用,并不需要任何操作。

每条go语句一般携带一个函数调用,被称为go函数。而主 goroutine 的go函数就是那个作为程序入口的main函数。

注意: go函数真正被执行的时间总会与其go语句被执行的时间不同。当执行到一条go语句的时候,Go 语言的运行时系统,会先试图从存放空闲的 G 的队列中获取一个 G(也就是 goroutine),找不到空闲 G 的情况下才会去创建一个新的 G。

找到空闲的 G 之后,Go 语言运行时系统会用这个 G 去包装当前的那个go函数(或者说该函数中的那些代码),再把这个 G 追加到可运行的 G 的队列中。这类队列中的 G 总是会按照先入先出的顺序,很快地由运行时系统内部的调度器安排运行。

只要go语句本身执行完毕,Go 程序完全不会等待go函数的执行,它会立刻去执行后边的语句。这就是所谓的异步并发地执行

代码中的for语句会以很快的速度执行完毕。当它执行完毕时,那 10 个包装了go函数的 goroutine 往往还没有获得运行的机会。一旦主 goroutine 中的代码(也就是main函数中的那些代码)执行完毕,当前的 Go 程序就会结束运行。

主 goroutine 等待

  • time.Sleep(time.Millisecond * 500) 暂停运行一段时间,直到到达指定的恢复运行时间。

  • 使用 通道阻塞 让主 goroutine 进入等待状态。好处是可以预估时间

    num := 10
    sign := make(chan struct{}, num)
    
    for i := 0; i < num; i++ {
        go func() {
            fmt.Println(i)
            sign <- struct{}{}
        }()
    }
    
    for j := 0; j < num; j++ {
        <-sign
    }
    

    类型字面量struct{}有些类似于空接口类型interface{},它代表了既不包含任何字段也不拥有任何方法的空结构体类型。

控制 goroutine 执行顺序

for i := 0; i < 10; i++ {
    go func(i int) {
        fmt.Println(i)
    }(i)
}

保证每个 goroutine 都可以拿到一个唯一的整数。go语句被执行时,我们传给go函数的参数i会先被求值,如此就得到了当次迭代的序号。之后,无论go函数会在什么时候执行,这个参数值都不会变。

Go 调度器

Go 语言不但有着独特的并发编程模型,以及用户级线程 goroutine,还拥有强大的用于调度 goroutine、对接系统级线程的调度器。

调度器是 Go 语言运行时系统的重要组成部分,它主要负责统筹调配 Go 并发编程模型中的三个主要元素,即:G(goroutine 的缩写)、P(processor 的缩写)和 M(machine 的缩写)。

Go 需要调度器原因:

  1. 系统原生线程有很多特性对于Go程序来说都是累赘。
  2. 基于Go语言模型,系统的调度决定并不一定合理。例如,Go的垃圾回收需要内存处于一致性的状态,这需要所有运行的线程都停止。垃圾回收的时间点是不确定的,如果仅由OS来调度,将会由大量的线程停止工作。
  3. 单独开发一个Go的调度器能保证内存处于一致性状态。当开始垃圾回收时,运行时只需要为当时正在CPU核上运行的那个线程等待即可,而不是等待所有的线程。

任何函数只需加上 go 就能送给调度器运行。调度器将goroutine分配到工作线程中运行,涉及3种类型的对象:

  • G goroutine,拥有自己的栈,程序计数器(instruction counter)和一些关于goroutine调度的信息(如正在阻塞的channel)。每个goroutine都是由一个 G结构 来表示。
  • M 工作线程即os线程,M代表 machine,指代的就是系统级线程。
  • P 代表processor,表示调度的上下文。一种用来运行go代码的抽象资源,最大数目不能超过GOMAXPROCS,在运行go代码时需要关联一个M

从宏观上说,G 和 M 由于 P 的存在可以呈现出多对多的关系。当一个正在与某个 M 对接并运行着的 G,需要因某个事件(比如等待 I/O 或锁的解除)而暂停运行的时候,调度器总会及时地发现,并把这个 G 与那个 M 分离开,以释放计算资源供那些等待运行的 G 使用。

而当一个 G 需要恢复运行的时候,调度器又会尽快地为它寻找空闲的计算资源(包括 M)并安排运行。另外,当 M 不够用时,调度器会帮我们向操作系统申请新的系统级线程,而当某个 M 已无用时,调度器又会负责把它及时地销毁掉。

M、P 和 G 之间的交互

在这里插入图片描述

可以看到,Go运行时存在两种类型的queue: 一种是一个全局的queue(在schedt结构体中,很少用到), 一种是每个P都维护自己的G的queue。

为了运行goroutine, M需要持有上下文PM会从P的queue弹出一个goroutine并执行。

当你创建一个新的goroutine的时候(go func()方法),它会被放入P的queue,当M执行了一些G后,如果它的queue为空,它会随机的选择另外一个P,从它的queue中取走一半的G到自己的queue中执行。

当你的goroutine执行阻塞的系统调用的时候(syscall),阻塞的系统调用会中断(intercepted),如果当前有一些G在执行,运行时会把这个线程从P中摘除(detach),然后再创建一个新的操作系统的线程(如果没有空闲的线程可用的话)来服务于这个P。当系统调用继续的时候,这个goroutine被放入到本地运行queue,线程会park它自己(休眠), 加入到空闲线程中。

goroutine特点

不需要在定义时区分是否是异步函数

调度器在合适的点进行切换

使用 -race 来检测数据访问冲突

goroutine 可能的切换点

  • I/O, select
  • channel
  • 等待锁
  • 函数调用(有时)
  • runtime.Goshed()
  • 只是参考,不能保证切换,不能保证在其他地方不切换

其他语言的协程

  • C++: Boost.Coroutine
  • Java: 不支持协程
  • python:
    • 使用 yield 关键字实现协程
    • Python 3.5 加入 async defer 对协程的原生支持

参考资料

Goroutine(协程)的理解
Golang调度器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值