Go语言基础之并发(Goroutine)

  • Post author:
  • Post category:其他


并发是编程里面一个非常重要的概念,Go语言在语言层面天生支持并发,这也是Go语言流行的一个很重要的原因。



并发与并行

并发:

同一时间段内执行多个任务

(我边微信和女朋友聊天边玩王者荣耀。我在自己复活阶段这个短暂的时间去回复一下微信消息。并不意味着我能在同一时刻玩游戏和陪女朋友)。

并行:

同一时刻执行多个任务

(我自己去玩王者荣耀时候。我写了一个智能聊天助手,它可以根据我和女朋友的聊天记录,自动生成,合适的消息并进行回复。如此,我便可以在玩游戏的同一时刻和女朋友聊天)。

可以看出在并行中,不止需要一个干活的人,对于操作系统来说,这个干活的人就是CPU。对于现代计算机来说多是多核,从而可以实现并行,而在单核时代的时间片轮换实质上是并发的串行在单核CPU上执行。



Go语言实现并发的方式


Go语言的并发通过goroutine实现。

goroutine类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个goroutine并发工作。goroutine是由Go语言的运行时(runtime)调度完成,而线程是由操作系统调度完成。


Go语言还提供channel在多个goroutine间进行通信。

goroutine和channel是 Go 语言秉承的 CSP(Communicating Sequential Process)并发模式的重要实现基础。



goroutine的使用

Go语言中的goroutine就是这样一种机制,goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。

简单将Goroutine归纳为协程并不合适,因为它运行时会创建多个线程来执行并发任务,且任务单元可被调度到其它线程执行。这更像是多线程和协程的结合体,能最大限度提升执行效率,发挥多核处理器能力。

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func chat(who string) {
	for {
		n := rand.Int31n(5) //生成[0,5)的随机数
		switch n {
		case 0:
			fmt.Println(who, ",我想你了,你在干嘛?", who)
		case 1:
			fmt.Println(who, ",我想你了,我在和小可爱聊天呀")
		case 2:
			fmt.Println(who, ",你今天有什么有趣的跟我分享吗?")
		case 3:
			fmt.Println(who, ",哇啊,还是我的小可爱机智过人,比心")
		case 4:
			fmt.Println(who, ",亲爱的,你真漂亮")
		default:
			fmt.Println(who, ",I love you!!!")
		}
		time.Sleep(time.Second * 20)
	}
}
func main() {
	who := "girlFriend"
	go chat(who) //聊天机器人去聊天
	for {
		fmt.Println("我在王者峡谷") //沉迷于王者无法自拨
		time.Sleep(time.Second * 5)
	}
}

这里 go关键字就是让Go语言去启动一个goroutine。这个goroutine的执行流就是go关键字后面的函数。可能是由于妈妈没在家,我已经在王者峡谷玩hi了,不能让我一直沉迷于游戏,妈妈还是要出现的。



goroutine与线程


可增长的栈


OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,他可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这个大。所以在Go语言中一次创建十万左右的goroutine也是可以的。


Go语言中的操作系统线程和goroutine的关系:

  1. 一个操作系统线程对应用户态多个goroutine。
  2. go程序可以同时使用多个操作系统线程。
  3. goroutine和OS线程是多对多的关系,即m:n。



goroutine调度

GPM是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。

G: 代表一个goroutine对象,每次go调用的时候,都会创建一个G对象,它包括栈、指令指针以及对于调用goroutines很重要的其它信息,比如阻塞它的任何channel。

M:代表一个线程,每次创建一个M的时候,都会有一个底层线程创建;所有的G任务,最终还是在M上执行。

P:代表一个处理器,每一个运行的M都必须绑定一个P,就像线程必须在一个CPU核上执行一样,由P来调度G在M上的运行,P的个数就是GOMAXPROCS(最大256),启动时固定的,一般不修改;M的个数和P的个数不一定一样多(会有休眠的M或者不需要太多的M)(最大10000);每一个P保存着本地G任务队列,也有一个全局G任务队列。


单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。


了解更多


GOMAXPROCS


Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。

Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。

Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。

runtime.GOMAXPROCS(3)



版权声明:本文为Vickers_xiaowei原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。