导语

我们都知道Golang的最大特性就是Goroutine(协程)的设计,那究竟什么是协程呢?它和计算机中的进程和线程又有哪些区别呢?为什么协程可以同时并发,几乎不占内存,想必在你的心中有了类似这样很多的疑问,那么现在我们一起来看看这些是为什么吧。

进程和线程的说明

在了解协程之前,我们需要回顾一下计算机操作系统的知识

  • 进程就是程序在操作系统中的依次执行过程,是系统进行资源分配和调度的基本单位。
  • 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位
  • 一个进程可以创建和销毁多个线程,同时一个进程中的多个线程可以并发执行
  • 一个程序至少有一个进程,一个进程至少有一个线程
    为了更好的理解,我们画一张图:


    image

    那么并发和并行又有啥区别呢?

  • 多线程程序在单核上运行就是并发
  • 多线程程序在多核上与运行就是并行

并发:在一个CPU上,比如说有10个线程,每个线程执行10毫秒(进行轮询)从我们的角度看,这10个线程似乎同时在循行,但是实际上某个时间点只有一个线程在运行,就是并发
并行:现在反过来,我们有10个CPU,有10个线程,这10个线程运行在不同的CPU上,互不干扰就是并行。

Go协程

了解了以上进程和线程、并发与并行,那么Go协程又是什么呢?在一个Go程序中,我们通常成main函数对应了我们的主线程,而在主线程,开辟的一个轻量级的线程则称之为协程。下面是Go协程的特点

  • 独立的栈空间:所谓栈空间就是线程独有的,保存其运行状态和局部自动变量的
  • 共享程序堆空间:大家共有的空间,用完需要及时的还给计算机。
  • 调度由用户控制:用户可以任意控制线程间的切换。开启和结束
  • 轻量级的线程:属于线程,不占用太多的CPU,但可以充分利用CPU,完成相应功能。

现在我们简单画一个主线程和协程的运行流程图来描述它们的关系


image

在上面的这个图中,可以可看到协程的运行是在主线程的控制下,因此若控制一个多个协程,就要学会阻塞的相关知识。因为Go的线程轻量级,属于逻辑态,所以可以轻松启动上万个协程,而向我们所熟知的C和Java的线程是属于内核态,往往几千个线程就会耗光CPU。

协程模型原理剖析

MPG模式介绍

一个G的执行需要P和M的支持,M与P关联后,就会形成一个有效的G运行环境:工作线程+上下文环境。


image
  1. M为操作系统的主线程,又叫工作线程,用来关联上下文环境P,如果没有足够的工作线程,就会创建新的M
  2. Go运行时会适时的让上下文环境P与不同的工作线程M建立连接或者断开,以使P中的可运行G即使获得运行时机。
  3. 一个G代表一个Go协程goroutine,即go函数。go语句会被传递为内部函数newproc的调用。
    下面用图来做更详细的解释:


    image

    在上面的这张图中我们可以看到程序有3个M,如果三个M都在一个cpu上就是我们所说的并发,不在同一个就是并行,三个M正在执行相关的G,但是不同的是,在相关G的后面有着不同数量的G正在等待。接着我们再看一张图


    image

    仔细观察我们会发现原来M2的3个G跑到了M1下,其实在程序运行的过程中会发生阻塞,因此当M2出现阻塞,G就会找相应空闲的G,所以可以充分利用我们的线程,而不会造成资源浪费。如果这时M2不阻塞,就会归为空闲的G等待着被唤醒。

陷阱

你可能会发现并发的时候会出现相应的资源争夺问题,比如有一个数组,当协程1要去写入内容的时候,协程2要读内容,这样就会造成数组不知道要写还是读,数据会出现严重的不一致,因此在我们用协程去解决问题的时候,应该给我们的协程加上锁,这样就不会出现同一个资源出现争夺的情况,Go中同样也提供了管道,并且线程安全,来保障我们的资源不会出现争夺。

推荐阅读

  • 公众号【常更新】:无崖子天下无敌
  • 博客地址【定期更新】:https://mowuya.cn/