前言
又是快乐的周五,按照往常的习惯,周五是不可能好好的工作,不摸它一天的🐟都对不起这个好日子,但是平白无故的摸鱼又有一定的罪恶感,而且还需要防范主管的巡视,但是!!!!
事情总会有漏洞,比如说当我在写文章,虽然也是在干着跟工作无关的内容,但是!!却可以心安理得的,因为这是一个学习的过程,而只有更好的学习,才可以回报公司滴!!(必须这样想)那么话不多说,开始今天的内容。
并发与并行
在聊并发和并行的操作的时候,必须了解什么是操作系统的进城,线程以及Go中的goroutine。
这里有我之前写的一篇小文章(juejin.cn/post/695249…)
再聊到Go中的并发的时候,要先了解一下并发和并行的区别。上面这张图已经说明了并发和并行一个问题,其实可以这么理解——
并发:在一个时间段内完成处理一些事情
并行:在一个时间点内同时做一些事情
通过上图也是看得出来,其实并发就是一个咖啡机交替性的去处理两个等待队列的事情,只不过因为处理的非常的快,所以很多时候,用户感觉跟并行一样。
上图展示了一个包含了所有可以分配的资源的进程。其中可以看到一个线程就是一个执行的空间,它被操作系统所调用,用来执行相对于的一些函数等。
操作系统会在物理层面去调用线程来运行,而在Go中,则是由逻辑处理器来(也就是函数),来控制和调度goroutine,其中在Go语言运行的时候,默认的,会为每一个物理处理器分配一个逻辑处理器,这些逻辑处理器是用来并发的调度所有的被创建出来的goroutine的。
我们知道对于多线程来说,线程的调度是一件很耗能的事情,但是在Go中goroutine却不存在这样的问题,虽然本质上来说都是让一个goroutine暂时挂起,然后让其他的goroutine继续去工作,但是因为逻辑处理器在调度goroutine的时候,并不需要进入到线程调度中的内核上下文中去,所以这里所需要的代价就小了很多。
GMP模型
说到goroutine就必须说道Go语言中的GMP模型
从上图我们可以看到M指的是执行的线程,P指的是逻辑处理器,G指的就是goroutine,当Go语言运行的时候,逻辑处理器会绑定到一个操作系统的线程上去,然后当goroutine可以运行的时候,会被放入逻辑处理器的执行队列中,通过逻辑处理器,将一个个goroutine供给给线程去执行。
当执行到这个goroutine发生阻塞的时候(例如一些打开文件什么的),逻辑处理器会从该线程中分离分离出来,然后到一个新的线程上去,继续运行整个服务,而原来的线程则会继续阻塞,等待系统的调用返回。
注意:如果goroutine进行的是一次IO网络调用的话,虽然也是会发生阻塞,但跟上面的情况又有些不一样。
首先goroutine会和逻辑处理器分离开来,然后所有的goroutine会移植到网路轮询器上面去运行,当goroutine完成了对于网络的读或者写的时候,它才被重新放回逻辑处理器的队列上去。
深入理解goroutine
让我们先来看一段代码
func main() {
var w sync.WaitGroup // 用来等待程序的完成
w.Add(2) //数字为2,表示有两个goroutine在运行
go func() {
defer w.Done()//函数运行结束的时候,通知main函数,想当与Add(-1)
for i:=0;i<3 ;i++ {
for char:='A';char<'A'+26 ;char++ {
fmt.Printf("%c",char)
//打印大写字母A~Z
}
}
}()
go func() {
defer w.Done()
for i:=0;i<3 ;i++ {
for char:='a';char<'a'+26 ;char++ {
fmt.Printf("%c",char)
//打印小写字母a~z
}
}
}()
w.Wait()//等待goroutine的结束,在add不为0的时候阻塞
}
首先我们要知道,main函数其实也是一个goroutine,不过它是最主要的一个,当main函数退出的时候,里面的goroutine就算没有执行完成,也会被系统给回收,所以这里我们需要用到sync.WaitGroup来等待goroutine的完成后,main函数再退出。
而当我们执行上面那段代码的时候,结果是这样的
abcdefghijABCDEFGHIJKLMklmnopqrstuvwxyzabNOPQRSTUVWX......
产生这样的结果的原因,是因为这两个goroutine是在并行执行的,因为在运行的Go的时候,默认情况下,我的电脑是一台八核的电脑,他会启用八条进程(每个进程内有一条线程)同时的来调度Go函数,也就是会有多个逻辑处理器,同时来处理这些函数,而这也就是它并行的原因。
注意:只有当存在多个逻辑处理器,并且让每个goroutine运行在一个独立的逻辑处理器上面的时候,goroutine才会并行运行。
当逻辑处理器只有一个的时候,又会出现什么情况?我们同样的使用上面的代码,不过需要加多一行,来限制它的逻辑处理器数量(也就是会有多少个线程操作这次Go程序)
func main() {
var w sync.WaitGroup // 用来等待程序的完成
runtime.GOMAXPROCS(1)// 限制执行的线程只有一条
w.Add(2) //数字为2,表示有两个goroutine在运行
go func() {
defer w.Done()//函数运行结束的时候,通知main函数
for i:=0;i<3 ;i++ {
for char:='A';char<'A'+26 ;char++ {
fmt.Printf("%c",char)
}
}
}()
go func() {
defer w.Done()
for i:=0;i<3 ;i++ {
for char:='a';char<'a'+26 ;char++ {
fmt.Printf("%c",char)
}
}
}()
w.Wait()//等待goroutine的结束
}
它的执行结果是这样的
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLM
我们可以看到,它是先输出了三次小写的a-z,然后再输出大写的A~Z,这是因为只有一个逻辑处理器的时候,第一个goroutine很快执行完了,然后它的输出被放到储存的栈里面去,我们知道栈是后进先出的,所以它会先输出三遍第二个的goroutine的小写字母,然后在输出大写字母。
当时,当一个goroutine持续的时间比较长的时候又会怎么样呢,同样的我们也是使用上面的代码,只不过我们把循环加大到30000次,执行的结果就会变成这样
前面忽略一部分
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkABCDEFGHIJKLMNOPQRSTUVW
中间再忽略一部分
stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv
我们可以看到它在打印了很长的一段小写字母的a~z之后,它会切换到去打印大写字母,然后又切换回来打印小写字母。
原因是逻辑处理器为了避免一个goroutine在执行的时候,占用的时间过长,当这个goroutine运行的时间过于漫长的时候,它会停止当前的goroutine,转而给其他goroutine运行的机会,所以就会有上面的情况出现。
最后
goroutine 可以说是整个Go语言的核心,但是它也非常的难以理解,例如我们知道goroutine是有伸缩性的,最大它可以渠道2G差不多,而这也就会扩展出非常多的线程,但是,当goroutine执行完成被回收的时候,这些扩展出去的线程是怎么处理的,Go的垃圾回收机制没有回收线程的这一个说法。(这点我还不知道—_-)
还有我上面所提到的所有对于goroutine的运用,都是没有存在读写操作的,那么在存在读写操作的时候,Go语言又会通过怎么样的形式,来破解那些并发的问题?(这点我会在下一篇文章说明)
最后最后,也是最重要的,我就快升级了!!!求看到了这里的看官大老爷给个赞呀!!!