Go 并发编程协程及调度机制详情

目录
  • 协程的概念
  • goroutine 的诞生
  • 使用 goroutine 加快速度
  • goroutine 的机制原理

前言:

协程(coroutine)是 Go 语言最大的特色之一,goroutine 的实现其实是通过协程。

协程的概念

协程一词最早出现在 1963 年发表的论文中,该论文的作者为美国计算机科学家 Melvin E.Conway。著名的康威定律:“设计系统的架构受制于产生这些设计的组织的沟通结构。” 也是这个作者。

协程是一种用户态的轻量级线程,可以想成一个线程里面可以有多个协程,而协程的调度完全由用户控制,协程也会有自己的 registers、context、stack 等等,并且由协程的调度器来控制说目前由哪个协程执行,哪个协程要被 block 住。

而相对于 Thread 及 Process 的调度,则是由 CPU 内核去进行调度,因此操作系统其实会有所谓许多的调度算法,并且可以进行抢占式调度,可以主动抢夺执行的控制权。

反之,协程是不行的,只能进行非抢占式的调度。 可以理解成,如果 coroutine 被 block 住,则会在用户态直接切换另外一个 coroutine 给此 thread 继续执行,这样其他 coroutine 就不会被 block 住,让资源能够有效的被利用,借此实现 Concurrent 的概念。

协程与线程

线程 是 CPU 调度的基本单位,多个线程可以通过共享进程的资源,通过共享内存等方式来进行线程间通信。

协程 可理解为轻量级线程,与线程相比,协程不受操作系统系统调度,协程调度由用户应用程序提供,协程调度器按照调度策略把协程调度到线程中运行。

  • 协程只需花几 KB 就可以被创立,线程则需要几 MB 的内存才能创立
  • 切换开销方面,协程远远低于线程,切换的速度也因此大幅提升

goroutine 的诞生

Golang 语言的 goroutine 其实就是协程,特别的是在语言层面直接原生支持创立协程,并在 runtime、系统调用等多方面对 goroutine 调度进行封装及处理。

相对于 Java 的建立线程,操作系统是会直接建立一个线程与其对应,而多个线程的间互相切换需要通过内核线程来进行,会有较大的上下文切换开销,造成的资源耗费,而 goroutine 是在代码上直接实现切换,不需要经过内核线程。

goroutine 的优势:

  • 与线程相比, goroutine 非常便宜,可以根据应用程序的需求自动分配, 但在线程的大小通常是固定的
  • 使用 goroutine 访问共享内存的时候 透过 channel 可以避免竞态条件的发生

比如,我们计算一个数字的质数,可以写出如下的代码:

package main

import (
	"fmt"
	"time"
)

func main() {
	num := 300000
	start := time.Now()
	for i := 1; i <= num; i++ {
		if isPrime(i) {
			fmt.Println(i)
		}
	}
	end := time.Now()
	fmt.Println(end.Unix()-start.Unix(), "seconds")
}
func isPrime(num int) bool {
	if num == 1 {
		return false
	} else if num == 2 {
		return true
	} else {
		for i := 2; i < num; i++ {
			if num%i == 0 {
				return false
			}
		}
		return true
	}
}

上面的代码用 num := 300000 来测试,也就是从 1~300000 之间来看那些数字会是质数,如果是质数的话就把质数输出,最后看到最终花费了 37 秒。运行结果如下:

使用 goroutine 加快速度

package main

import (
	"fmt"
	"time"
)

func main() {
	num := 300000
	start := time.Now()
	for i := 1; i <= num; i++ {
		go findPrimes(i)
	}

	end := time.Now()
	time.Sleep(5 * time.Second)
	fmt.Println(end.Unix()-start.Unix(), "seconds")
}

func findPrimes(num int) {
	if num == 1 {
		return
	} else if num == 2 {
		fmt.Println(num)
	} else {
		for i := 2; i < num; i++ {
			if num%i == 0 {
				return
			}
		}
		fmt.Println(num)
	}
}

go findPrimes 这条语句就可以开启一个 goroutine,因此以主程序来说这样等于是开启 300000 个 goroutine 来各自判断自己拿到 num 是不是质数这样。

用 time. Sleep 来休息五秒来让 main 主程序不要被关闭,否则由于开启 goroutine 之后代码会继续往下执行,如果没做 sleep 的话会导致主程序关闭,主程序一关闭 goroutine 就跟着关闭了,我们就看不出效果了。

这边运行之后会发现输出的质数出现并不是从小到大的,这是因为这些 goroutine 是一起做事情的,所以谁先做完谁就先输出这样。

运行结果如下,最后花费了大概 11 秒:

goroutine 的机制原理

理解 goroutine 机制的原理,关键是理解 Go 语言是如何实现调度器模型的。

计算机科学领域的任何问题都可以通过添加间接中间层来解决。GPM 模型就是这一理论的实践者。

Go 语言中支撑整个调度器实现的主要有 4 个重要结构,分别是 machine(简称 M )、goroutine(简称 G )、processor(简称 P )、Scheduler(简称 Sched), 前三个定义在 runtime.h 中,Sched 定义在 proc.c 中。

  • Sched 结构就是调度器,它维护有存储 M 和 G 的队列以及调度器的一些状态信息等
  • M 结构是 Machine,系统线程,它由操作系统管理和调度的,goroutine 就是跑在 M 之上的; M 是一个很大的结构,里面维护小对象内存 cache(mcache)、当前执行的 goroutine、随机数发生器等等非常多的信息。
  • P 结构是 Processor,处理器,它的主要用途就是用来执行 goroutine 的,它维护了一个 goroutine 队列,即 runqueue。 Processor 是让我们从 N:1 调度到 M:N 调度的重要部分。
  • G 是 goroutine 实现的核心结构,它包含了栈,指令指针,以及其他对调度 goroutine 很重要的信息,例如其阻塞的 channel。

我们分别用三角形,矩形和圆形表示 Machine Processor 和 Goroutine:

在单核处理器的场景下,所有 goroutine 运行在同一个 M 系统线程中,每一个 M 系统线程维护一个 Processor,任何时刻,一个 Processor 中只有一个 goroutine,其他 goroutine 在 runqueue 中等待。

一个 goroutine 运行完自己的时间片后,让出上下文,回到 runqueue 中。 多核处理器的场景下,为了运行 goroutines,每个 M 系统线程会持有一个 Processor 。

可以看到 Go 的并发用起来非常简单,用了一个语法糖将内部复杂的实现结结实实的包装了起来。

其内部可以用下面这张图来概述:

在单核处理器的场景下,所有 goroutine 运行在同一个 M 系统线程中,每一个 M 系统线程维护一个 Processor,任何时刻,一个 Processor 中只有一个 goroutine,其他 goroutine 在 runqueue 中等待。一个 goroutine 运行完自己的时间片后,让出上下文,回到 runqueue 中。 多核处理器的场景下,为了运行 goroutines,每个 M 系统线程会持有一个 Processor 。

在正常情况下,scheduler 会按照上面的流程进行调度,但是线程会发生阻塞等情况,看一下goroutine对线程阻塞等的处理。

到此这篇关于Go 并发编程协程及调度机制详情的文章就介绍到这了,更多相关Go 调度机制内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • golang协程设计及调度原理

    目录 一.协程设计-GMP模型 1.工作线程M 2.逻辑处理器p 3.协程g 4.全局调度信息schedt 5.GMP详细示图 二.协程调度 1.调度策略 获取本地运行队列 获取全局运行队列 协程窃取 2.调度时机 主动调度 被动调度 抢占调度 一.协程设计-GMP模型 线程是操作系统调度到CPU中执行的基本单位,多线程总是交替式地抢占CPU的时间片,线程在上下文的切换过程中需要经过操作系统用户态与内核态的切换.golang的协程(G)依然运行在工作线程(M)之上,但是借助语言的调度器,协程只需

  • golang协程与线程区别简要介绍

    目录 一.进程与线程 二.并发与并行 三.go协程与线程 1.调度方式 2.调度策略 3.上下文切换速度 4.栈的大小 四.GMP模型 一.进程与线程 进程是操作系统资源分配的基本单位,是程序运行的实例.例如打开一个浏览器就开启了一个进程. 线程是操作系统调度到CPU中执行的基本单位.例如在浏览器里新建一个窗口就需要一个线程来进行处理. 在一般情况下,线程是进程的组成部分,一个进程可以包含多个线程.例如浏览器可以新建多个窗口. 进程中的多个线程并发执行并共享进程的内存等资源.例如多个窗口之间可以

  • go语言中的协程详解

    协程的特点 1.该任务的业务代码主动要求切换,即主动让出执行权限 2.发生了IO,导致执行阻塞(使用channel让协程阻塞) 与线程本质的不同 C#.java中我们执行多个线程,是通过时间片切换来进行的,要知道进行切换,程序需要保存上下文等信息,是比较消耗性能的 GO语言中的协程,没有上面这种切换,一定是通过协程主动放出权限,不是被动的. 例如: C# 中创建两个线程 可以看到1和2是交替执行的 Go语言中用协程实现一下 runtime.GOMAXPROCS(1) 这个结果就是 执行了1 在执

  • Go简单实现协程池的实现示例

    目录 MPG模型 通道的特性 首先就是进程.线程.协程讲解老三样. 进程: 本质上是一个独立执行的程序,进程是操作系统进行资源分配和调度的基本概念,操作系统进行资源分配和调度的一个独立单位. 线程: 是操作系统能够进行运算调度的最小单位.它被包含在进程之中,是进程中的实际运作单位.一个进程中可以并发多个线程,每条线程执行不同的任务,切换受系统控制. 协程:  又称为微线程,是一种用户态的轻量级线程,协程不像线程和进程需要进行系统内核上的上下文切换,协程的上下文切换是由用户自己决定的,有自己的上下

  • Go简单实现协程方法

    目录 为什么需要协程 协程的本质 协程如何在线程中执行 GMP调度模型 协程并发 为什么需要协程 协程的本质是将一段数据的运行状态进行打包,可以在线程之间调度,所以协程就是在单线程的环境下实现的应用程序级别的并发,就是把本来由操作系统控制的切换+保存状态在应用程序里面实现了. 所以我们需要协程的目的其实就是它更加节省资源.可以在有限的资源内支持更高的并发,体现在以下三个方面: 资源利用:程可以利用任何的线程去运行,不需要等待CPU的调度. 快速调度:协程可以快速地调度(避开了系统调用和切换),快

  • Go 并发编程协程及调度机制详情

    目录 协程的概念 goroutine 的诞生 使用 goroutine 加快速度 goroutine 的机制原理 前言: 协程(coroutine)是 Go 语言最大的特色之一,goroutine 的实现其实是通过协程. 协程的概念 协程一词最早出现在 1963 年发表的论文中,该论文的作者为美国计算机科学家 Melvin E.Conway.著名的康威定律:“设计系统的架构受制于产生这些设计的组织的沟通结构.” 也是这个作者. 协程是一种用户态的轻量级线程,可以想成一个线程里面可以有多个协程,而

  • Python并发编程协程(Coroutine)之Gevent详解

    Gevent官网文档地址:http://www.gevent.org/contents.html 基本概念 我们通常所说的协程Coroutine其实是corporateroutine的缩写,直接翻译为协同的例程,一般我们都简称为协程. 在linux系统中,线程就是轻量级的进程,而我们通常也把协程称为轻量级的线程即微线程. 进程和协程 下面对比一下进程和协程的相同点和不同点: 相同点: 我们都可以把他们看做是一种执行流,执行流可以挂起,并且后面可以在你挂起的地方恢复执行,这实际上都可以看做是con

  • Go 并发实现协程同步的多种解决方法

    go 简洁的并发 多核处理器越来越普及.有没有一种简单的办法,能够让我们写的软件释放多核的威力?是有的.随着Golang, Erlang, Scala等为并发设计的程序语言的兴起,新的并发模式逐渐清晰.正如过程式编程和面向对象一样,一个好的编程模式有一个极其简洁的内核,还有在此之上丰富的外延.可以解决现实世界中各种各样的问题.本文以GO语言为例,解释其中内核.外延. 前言 Java 中有一系列的线程同步的方法,go 里面有 goroutine(协程),先看下下面的代码执行的结果是什么呢? pac

  • Kotlin 协程的取消机制详细解读

    目录 引言 协程的状态 取消协程的用法 协程取消的有效性 如何写出可以取消的代码 在 finally 中释放资源 使用不可取消的 block CancellationException 超时取消 异步的超时和资源 取消检查的底层原理 引言 在 Java 语言中提供了线程中断的能力,但并不是所有的线程都可以中断的,因为 interrupt 方法并不是真正的终止线程,而是将一个标志位标记为中断状态,当运行到下一次中断标志位检查时,才能触发终止线程. 但无论如何,终止线程是一个糟糕的方案,因为在线程的

  • python协程与 asyncio 库详情

    目录 1.asyncio 异步 I/O 库 异步函数的定义 事件循环 event_loop 创建 task 回调返回值 循环事件关闭 2.本节爬虫项目 前言: python 中协程概念是从 3.4 版本增加的,但 3.4 版本采用是生成器实现,为了将协程和生成器的使用场景进行区分,使语义更加明确,在 python 3.5 中增加了 async 和 await 关键字,用于定义原生协程. 1.asyncio 异步 I/O 库 python 中的 asyncio 库提供了管理事件.协程.任务和线程的

  • Java并发编程之显式锁机制详解

    我们之前介绍过synchronized关键字实现程序的原子性操作,它的内部也是一种加锁和解锁机制,是一种声明式的编程方式,我们只需要对方法或者代码块进行声明,Java内部帮我们在调用方法之前和结束时加锁和解锁.而我们本篇将要介绍的显式锁是一种手动式的实现方式,程序员控制锁的具体实现,虽然现在越来越趋向于使用synchronized直接实现原子操作,但是了解了Lock接口的具体实现机制将有助于我们对synchronized的使用.本文主要涉及以下一些内容: 接口Lock的基本组成成员 可重入锁Re

  • Python中协程用法代码详解

    本文研究的主要是python中协程的相关问题,具体介绍如下. Num01–>协程的定义 协程,又称微线程,纤程.英文名Coroutine. 首先我们得知道协程是啥?协程其实可以认为是比线程更小的执行单元. 为啥说他是一个执行单元,因为他自带CPU上下文.这样只要在合适的时机, 我们可以把一个协程 切换到另一个协程. 只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的. Num02–>协程和线程的差异 那么这个过程看起来和线程差不多.其实不然, 线程切换从系统层面远不止保存和恢复 CP

  • 浅析python协程相关概念

    这篇文章是读者朋友的python协程的学习经验之谈,以下是全部内容: 协程的历史说来话长,要从生成器开始讲起. 如果你看过我之前的文章python奇遇记:迭代器和生成器 ,对生成器的概念应该很了解.生成器节省内存,用的时候才生成结果. # 生成器表达式 a = (x*x for x in range(10)) # next生成值 next(a()) # 输出0 next(a()) # 输出1 next(a()) # 输出4 与生成器产出数据不同的是,协程在产出数据的同时还可以接收数据,具体来说就

  • Python协程asyncio 异步编程笔记分享

    目录 1.事件循环 2.协程和异步编程 2.1 基本使用 2.2 await 2.3 Task对象 1.事件循环 可以理解成为一个死循环,去检查任务列表中的任务,如果可执行就去执行,如果检查不到就是不可执行的,那就忽略掉去执行其他可执行的任务,如果IO结束了(比如说去百度下载图片,下载完了就会变成可执行任务)再去执行下载完成之后的逻辑 #这里的任务是有状态的,比如这个任务已经完成或者正在执行或者正在IO等待 任务列表 = [ 任务1, 任务2, 任务3,... ] while True: 可执行

  • Python协程asyncio异步编程笔记分享

    目录 1.事件循环 2.协程和异步编程 2.1基本使用 2.2await 2.3Task对象 1.事件循环 可以理解成为一个死循环,去检查任务列表中的任务,如果可执行就去执行,如果检查不到就是不可执行的,那就忽略掉去执行其他可执行的任务,如果IO结束了(比如说去百度下载图片,下载完了就会变成可执行任务)再去执行下载完成之后的逻辑 #这里的任务是有状态的,比如这个任务已经完成或者正在执行或者正在IO等待 任务列表 = [ 任务1, 任务2, 任务3,... ] while True: 可执行的任务

随机推荐