【Go语言学习】2019-04-24 协程初步讨论与简单扩展

 阅读约 6 分钟

grape

全部视频:https://segmentfault.com/a/11...

原视频地址:https://biglive.xueersi.com/L...

GO协程有关知识(扩展)

Go语言最大的特色就是从语言层面支持并发(Goroutine),Goroutine是Go中最基本的执行单元。事实上每一个Go程序至少有一个Goroutine:主Goroutine。当程序启动时,它会自动创建。

首先了解什么是协程,什么是线程

协程:又称微线程与子例程(或者称为函数)一样,协程(coroutine)也是一种程序组件。相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。和线程类似,共享堆,不共享栈,协程的切换一般由程序员在代码中显式控制。它避免了上下文切换的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂。而Goroutine可以理解为一种Go语言的协程。同时它可以运行在一个或多个线程上。

线程:轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程的切换一般也由操作系统调度。

区别:协程是运行在线程上的,在代码中显示控制,没有上下文切换所消耗的资源,协程拥有栈空间来存储自己的上下文,如果栈空间不足,则会更改其他的协程。

Go实现了两种并发形式。第一种是大家普遍认知的:多线程共享内存。其实就是Java或者C++等语言中的多线程开发。另外一种是Go语言特有的,也是Go语言推荐的:CSP(communicating sequential processes)并发模型。

CSP并发模型:以通信的方式来共享内存,传统的多线程之间以共享内存来进行通信,GO语言则以通信的方式来共享内存,那么就来说一说这个CSP模型,它是通过goroutine和channel来实现的。

- goroutine 是Go语言中并发的执行单位。有点抽象,其实就是和传统概念上的”线程“类似,可以理解为”线程“。
- channel是Go语言中各个并发结构体(goroutine)之前的通信机制。 通俗的讲,就是各个goroutine之间通信的”管道“,有点类似于Linux中的管道。

生成一个goroutine需要代码简简单单的go func()就可以生成,channel也是需要声明chan即可,同时,channel在使用时需要注意,在读和写的时候channel都是堵塞的,向管道里写或者读数据类似于流输入输出。

GO并发的实现原理

我们先从线程讲起,无论语言层面何种并发模型,到了操作系统层面,一定是以线程的形态存在的。而操作系统根据资源访问权限的不同,体系架构可分为用户空间和内核空间;内核空间主要操作访问CPU资源、I/O资源、内存资源等硬件资源,为上层应用程序提供最基本的基础资源,用户空间呢就是上层应用程序的固定活动空间,用户空间不可以直接访问资源,必须通过“系统调用”、“库函数”或“Shell脚本”来调用内核空间提供的资源。

我们现在的计算机语言,可以狭义的认为是一种“软件”,它们中所谓的“线程”,往往是用户态的线程,和操作系统本身内核态的线程(简称KSE),还是有区别的。也就是说无论你怎么去实现并发,其实根本上都是去调用内核态的线程,一切并发都是纸老虎~

线程模型的实现,可以分为以下几种方式:1.用户级线程模型,2.内核级线程模型,3.两级线程模型。而我们的GO线程模型就是一种特殊的两级线程模型(GPM调度模型)。

GPM调度模型:

  • G: 表示goroutine,存储了goroutine的执行stack信息、goroutine状态以及goroutine的任务函数等;
  • P: 表示逻辑processor,P的数量决定了系统内最大可并行的G的数量(前提:系统的物理cpu核数>=P的数量);P的最大作用还是其拥有的各种G对象队列、链表、一些cache和状态。
  • M: M代表着真正的执行计算资源。在绑定有效的p后,进入schedule循环;而schedule循环的机制大致是从各种队列、p的本地队列中获取G,切换到G的执行栈上并执行G的函数,调用goexit做清理工作并回到m,如此反复。M并不保留G状态,这是G可以跨M调度的基础。
  • GPM模型图,来源自https://segmentfault.com/a/1190000017568827
  • P是一个“逻辑Proccessor”,每个G要想真正运行起来,首先需要被分配一个P(进入到P的local runq中,这里暂忽略global runq那个环节)。对于G来说,P就是运行它的“CPU”,可以说:G的眼里只有P。但从Go scheduler视角来看,真正的“CPU”是M,只有将P和M绑定才能让P的runq中G得以真实运行起来。
  • G-P-M模型的实现算是Go scheduler的一大进步,但Scheduler仍然有一个头疼的问题,那就是不支持抢占式调度,导致一旦某个G中出现死循环或永久循环的代码逻辑,那么G将永久占用分配给它的P和M,位于同一个P中的其他G将得不到调度,出现“饿死”的情况。更为严重的是,当只有一个P时(GOMAXPROCS=1)时,整个Go程序中的其他G都将“饿死”。那怎么办呢?在GO1.2中,他增加了一个监控,Go程序启动时,runtime会去启动一个名为sysmon的m(一般称为监控线程),该m无需绑定p即可运行,该m在整个Go程序的运行过程中至关重要:向长时间运行的G任务发出抢占调度,收回因syscall长时间阻塞的P等等

如果G被阻塞在某个system call操作上,那么不光G会阻塞,执行该G的M也会解绑P(实质是被sysmon抢走了),与G一起进入sleep状态。如果此时有idle的M,则P与其绑定继续执行其他G;如果没有idle M,但仍然有其他G要去执行,那么就会创建一个新M。

参考文章:

  1. https://segmentfault.com/a/11...
  2. https://blog.csdn.net/weixin_...
阅读 911更新于 2019-05-21

推荐阅读
LNMPR学习笔记
用户专栏

LNMPR学习笔记

40 人关注
52 篇文章
专栏主页
目录