并发:同一时间段内执行多个任务(我边微信和女朋友聊天边玩王者荣耀。我在自己复活阶段这个短暂的时间去回复一下微信消息。并不意味着我能在同一时刻玩游戏和陪女朋友)。
并行:同一时刻执行多个任务(我自己去玩王者荣耀时候。我写了一个智能聊天助手,它可以根据我和女朋友的聊天记录,自动生成,合适的消息并进行回复。如此,我便可以在玩游戏的同一时刻和女朋友聊天)。
可以看出在并行中,不止需要一个干活的人,对于操作系统来说,这个干活的人就是CPU。对于现代计算机来说多是多核,从而可以实现并行,而在单核时代的时间片轮换实质上是并发的串行在单核CPU上执行。
Go语言的并发通过goroutine实现。 goroutine类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个goroutine并发工作。goroutine是由Go语言的运行时(runtime)调度完成,而线程是由操作系统调度完成。
Go语言还提供channel在多个goroutine间进行通信。 goroutine和channel是 Go 语言秉承的 CSP(Communicating Sequential Process)并发模式的重要实现基础。
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了,不能让我一直沉迷于游戏,妈妈还是要出现的。
可增长的栈
OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,他可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这个大。所以在Go语言中一次创建十万左右的goroutine也是可以的。
Go语言中的操作系统线程和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)