入门参考:从 Go 中的协程理解串行和并行

用户头像
soolaugust
关注
发布于: 2020 年 12 月 23 日
入门参考:从Go中的协程理解串行和并行

Go语言的设计亮点之一就是原生实现了协程,并优化了协程的使用方式。使得用Go来处理高并发问题变得更加简单。今天我们来看一下Go中的协程。

从串行到并行

在处理器还是单个单核的时候,这个时候并不存在并行,因为只有一个处理器。所以那时候的编程都是串行编程。程序执行都是从头顺序执行到尾。到了多处理器多核的时代,为了充分利用处理器的处理能力,开始出现了并发编程。开发者开始在进程中启用多个线程来执行操作,利用CPU的调度能力来最大化程序处理效率。

并发,并行

在说到并发编程的时候总会遇到这两个概念,面试的时候也会问道,在这里就简单说一下这两者的区别:



并发是一种能力,是指多个任务在一段时间内同时发生。

并行值得是多个任务同时发生,就是并行。



并发值得是并行的能力,并发不一定是同时发生,可能是同一时间段内交替发生。

进程,线程,协程

进程和线程是操作系统的基本概念:



进程:指计算机中已运行的程序,进程是程序的基本执行实体。

线程:是操作系统能够进行运算调度的最小单位。它被包含在进程中,是进程的实际运行单位。



那么协程是在线程之上,更加轻量级的设计。协程因为只工作在用户控件,没有线程上下文切换带来的消耗。协程的调度由用户手动切换,所以更加灵活。



协程的另一大优势就是因为在用户空间调度,所以不会出现代码执行一半被强制中断,所以无需原子操作锁。

Go中的协程

在Go中使用协程非常简单,就使用go关键字就可以了。我们来看一段串行代码使用协程如何进行操作:

package main
import (
"fmt"
"time"
)
func main(){
print1To10()
}
func print1To10(){
for i := 1; i<=10; i++{
fmt.Printf("%d ", i)
}
}
// 输出
// 1 2 3 4 5 6 7 8 9 10

那么使用协程,我们来看一下运行结果:

package main
import (
"fmt"
"time"
)
func main(){
fmt.Println("before go coroutine")
go print1To10()
fmt.Println("after go coroutine")
time.Sleep(100 * time.Millisecond) //防止主协程直接结束了,打印协程还没来得及执行
}
func print1To10(){
for i := 1; i<=10; i++{
fmt.Printf("%d ", i)
}
}
// 输出
/***********
before go coroutine
after go coroutine
1 2 3 4 5 6 7 8 9 10
*************/

我们可以看出使用go关键词后,打印并不是按照顺序串行执行的,而是在主协程执行结束后,打印协程才开始执行。

Go协程的调度机制

Go中的协程调度模型是G-P-M模型:



G代表Goroutine,也就是Go中的协程对象。

P代表Processor,代表虚拟的处理器。一般来说,和逻辑核一一对应。

M代表Machine,实际上是操作系统的线程。



image.png



这里我们简单说一下Go的调度机制,感兴趣或者有了解的可以自行看Go的源码:



  1. 在Go程序启动时,会给每个逻辑核分配一个P(虚拟处理器)

  2. 同时,Go会创建一个主协程G,来执行程序。新创建的G会被放到LRQ(P上的本地G队列)或者GRQ(全局G队列)。

  3. 给P分配一个M(内核线程),这些M由OS Scheduler调度而非Go Scheduler调度。M用来运行G

  4. P会尽可能获取G来运行,当没有G运行后,会销毁并重新进入调度



其中第4条 尽可能获取G 则是Go的有趣的设计理念之一,当一个 P 发现自己的 LRQ 已经没有 G 时,会从其他 P “偷” 一些 G 来运行。看看这是什么精神!自己的工作做完了,为了全局的利益,主动为别人分担。这被称为 Work-stealing



再看串行和并行

这里我们以Go协程来继续说一下串行和并行,对于习惯于串行编程的程序员来说,理解并行可能稍微需要点时间,对于程序设计来说,并行的设计主要是为了提高程序运行的效率,使得程序能够充分利用多核多处理器的资源(或者多机器)。那么对于如何充分利用,大部分支持并行编程的语言都有其内部的调度机制,即使没有,也会使用系统的调度机制--线程调度。



那么对于并行调度机制总体上分为两类:协作式和抢占式



协作式:一个任务得到了 CPU 时间,除非它自己放弃使用 CPU ,否则将完全霸占 CPU ,所以任务之间需要协作使用一段时间的 CPU ,放弃使用,其它的任务也如此,才能保证系统的正常运行。如果有一个任务死锁,则系统也同样死锁。



抢占式:总控制权在操作系统手中,操作系统会轮流询问每一个任务是否需要使用 CPU ,需要使用的话就让它用,不过在一定时间后,操作系统会剥夺当前任务的 CPU 使用权,把它排在询问队列的最后,再去询问下一个任务。如果有一个任务死锁,系统仍能正常运行。



在 Go1.1 版本中,调度器还不支持抢占式调度,只能依靠 goroutine 主动让出 CPU 资源,存在非常严重的调度问题。



Go1.12 中编译器在特定时机插入函数,通过函数调用作为入口触发抢占,实现了协作式的抢占式调度。但是这种需要函数调用主动配合的调度方式存在一些边缘情况。



后面Go在1.14版本实现了基于信号的抢占式调度。用于解决解决了垃圾回收和栈扫描时存在的问题。



Go的协程调度目前虽然不能称得上完美,但是对于我们理解并行有一定的帮助。所谓并行编程,就是开启多个任务而不用等待任务结果。可以使得相互独立的任务同时运行,比如文件写入等。



发布于: 2020 年 12 月 23 日阅读数: 381
用户头像

soolaugust

关注

公众号:雨夜随笔 2018.09.21 加入

公众号:雨夜随笔

评论

发布
暂无评论
入门参考:从Go中的协程理解串行和并行