《Go语言四十二章经》第二十一章 协程(goroutine)

栏目: Go · 发布时间: 6年前

内容简介:《Go语言四十二章经》第二十一章 协程(goroutine)作者:李骁Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.

《Go语言四十二章经》第二十一章 协程(goroutine)

作者:李骁

Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.

并发: 指的是程序的逻辑结构。如果程序代码结构中的某些函数逻辑上可以同时运行,但物理上未必会同时运行。 并行: 并行是指程序的运行状态。并行则指的就是在物理层面也就是使用了不同CPU在执行不同或者相同的任务。

21.1 并发

并发是在同一时间处理(dealing with)多件事情。并行是在同一时间做(doing)多件事情。并发的目的在于把当个 CPU 的利用率使用到最高。并行则需要多核 CPU 的支持。

Go 语言从语言层面上就支持了并发,goroutine是 Go 语言提供的一种用户态线程,有时我们也称之为协程。所谓的协程,某种程度上也可以叫做轻量线程,它不由os,而由应用程序创建和管理,因此使用开销较低(一般为4K)。我们可以创建很多的goroutine,并且它们跑在同一个内核线程之上的时候,就需要一个调度器来维护这些goroutine,确保所有的goroutine都使用cpu,并且是尽可能公平的使用cpu资源。调度器的主要有4个重要部分,分别是M、G、P、Sched,前三个定义在runtime.h中,Sched定义在proc.c中。

  • M (work thread) 代表了系统线程OS Thread,由操作系统管理。

  • P (processor) 衔接M和G的调度上下文,它负责将等待执行的G与M对接。P的数量可以通过GOMAXPROCS()来设置,它其实也就代表了真正的并发度,即有多少个goroutine可以同时运行。

  • G (goroutine) goroutine的实体,包括了调用栈,重要的调度信息,例如channel等。

在操作系统的OS Thread和编程语言的User Thread之间,实际上存在3中线程对应模型,也就是:1:1,1:N,M:N。

N:1 多个(N)用户线程始终在一个内核线程上跑,context上下文切换很快,但是无法真正的利用多核。 1:1 一个用户线程就只在一个内核线程上跑,这时可以利用多核,但是上下文切换很慢,切换效率很低。 M:N 多个goroutine在多个内核线程上跑,这个可以集齐上面两者的优势,但是无疑增加了调度的难度。

M:N 综合两种方式(N:1,1:1)的优势。多个 goroutines 可以在多个 OS threads 上处理。既能快速切换上下文,也能利用多核的优势,而Go正是选择这种实现方式。

Go 的goroutine是运行在虚拟CPU中的(通过runtime.GOMAXPROCS(1)所设定的虚拟CPU个数)。 虚拟CPU个数未必会和实际CPU个数相吻合。

每个goroutine都会被一个特定的P(虚拟CPU)选定维护,而M(物理计算资源)每次挑选一个有效P,然后执行P中的goroutine。

每个P会将自己所维护的goroutine放到一个G队列中,其中就包括了goroutine堆栈信息,是否可执行信息等等。

默认情况下,P的数量与实际物理CPU的数量相等。当我们通过循环来创建goroutine时,goroutine会被分配到不同的G队列中。 而M的数量又不是唯一的,当M随机挑选P时,也就等同随机挑选了goroutine。

所以,当我们碰到多个goroutine的执行顺序不是我们想象的顺序时就可以理解了,因为goroutine进入P管理的队列G是带有随机性的。

P的数量由runtime.GOMAXPROCS(1)所设定,通常来说它是和内核数对应,例如在4Core的服务器上会启动4个线程。G会有很多个,每个P会将goroutine从一个就绪的队列中做Pop操作,为了减小锁的竞争,通常情况下每个P会负责一个队列。

runtime.NumCPU()        // 返回当前CPU内核数
runtime.GOMAXPROCS(2)  // 设置运行时最大可执行CPU数
runtime.NumGoroutine() // 当前正在运行的goroutine 数

P维护着这个队列(称之为runqueue),Go语言里,启动一个goroutine很容易:go function 就行,所以每有一个go语句被执行,runqueue队列就在其末尾加入一个goroutine,在下一个调度点,就从runqueue中取出一个goroutine执行。

假如有两个M,即两个OS Thread线程,分别对应一个P,每一个P调度一个G队列。如此一来,就组成的goroutine运行时的基本结构:

  • 当有一个M返回时,它必须尝试取得一个P来运行goroutine,一般情况下,它会从其他的OS Thread线程那里窃取一个P过来,如果没有拿到,它就把goroutine放在一个global runqueue里,然后自己进入线程缓存里。

  • 如果某个P所分配的任务G很快就执行完了,这会导致多个队列存在不平衡,会从其他队列中截取一部分goroutine到P上进行调度。一般来说,如果P从其他的P那里要取任务的话,一般就取run queue的一半,这就确保了每个OS线程都能充分的使用。

  • 当一个OS Thread线程被阻塞时,P可以转而投奔另一个OS线程。

下面是G、 M、 P的具体结构,这不是Go代码:

struct  G
{
    uintptr stackguard0;// 用于栈保护,但可以设置为StackPreempt,用于实现抢占式调度
    uintptr stackbase;  // 栈顶
    Gobuf   sched;      // 执行上下文,G的暂停执行和恢复执行,都依靠它
    uintptr stackguard; // 跟stackguard0一样,但它不会被设置为StackPreempt
    uintptr stack0;     // 栈底
    uintptr stacksize;  // 栈的大小
    int16   status;     // G的六个状态
    int64   goid;       // G的标识id
    int8*   waitreason; // 当status==Gwaiting有用,等待的原因,可能是调用time.Sleep之类
    G*  schedlink;      // 指向链表的下一个G
    uintptr gopc;       // 创建此goroutine的Go语句的程序计数器PC,通过PC可以获得具体的函数和代码行数
};
struct P
{
    Lock;       // plan9 C的扩展语法,相当于Lock lock;
    int32   id;  // P的标识id
    uint32  status;     // P的四个状态
    P*  link;       // 指向链表的下一个P
    M*  m;      // 它当前绑定的M,Pidle状态下,该值为nil
    MCache* mcache; // 内存池
    // Grunnable状态的G队列
    uint32  runqhead;
    uint32  runqtail;
    G*  runq[256];
    // Gdead状态的G链表(通过G的schedlink)
    // gfreecnt是链表上节点的个数
    G*  gfree;
    int32   gfreecnt;
};
struct  M
{
    G*  g0;     // M默认执行G
    void    (*mstartfn)(void);  // OS线程执行的函数指针
    G*  curg;       // 当前运行的G
    P*  p;      // 当前关联的P,要是当前不执行G,可以为nil
    P*  nextp;  // 即将要关联的P
    int32   id; // M的标识id
    M*  alllink;    // 加到allm,使其不被垃圾回收(GC)
    M*  schedlink;  // 指向链表的下一个M
};

21.2 goroutine

在Go中,goroutine的使用很简单,直接在代码前加上关键字 go 即可。go关键字就是用来创建一个goroutine的,后面的代码块就是这个goroutine需要执行的代码逻辑。

package main

import (
	"fmt"
	"time"
)

func main() {
	for i := 1; i < 10; i++ {
		go func(i int) {
			fmt.Println(i)
		}(i)
	}
	// 暂停一会,保证打印全部结束
	time.Sleep(1e9)
}

有关于goroutine 之间的通信以及goroutine与主线程的控制,我们后续通过channel、context以及锁来进一步说明。

本书《Go语言四十二章经》内容在github上同步地址:https://github.com/ffhelicopter/Go42

本书《Go语言四十二章经》内容在简书同步地址: https://www.jianshu.com/nb/29056963

虽然本书中例子都经过实际运行,但难免出现错误和不足之处,烦请您指出;如有建议也欢迎交流。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

算法之美

算法之美

左飞 / 电子工业出版社 / 2016-3 / 79.00元

《算法之美——隐匿在数据结构背后的原理(C++版)》围绕算法与数据结构这个话题,循序渐进、深入浅出地介绍了现代计算机技术中常用的40 余个经典算法,以及回溯法、分治法、贪婪法和动态规划等算法设计思想。在此过程中,《算法之美——隐匿在数据结构背后的原理(C++版)》也系统地讲解了链表(包括单向链表、单向循环链表和双向循环链表)、栈、队列(包括普通队列和优先级队列)、树(包括二叉树、哈夫曼树、堆、红黑......一起来看看 《算法之美》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具