- Article -

理一理进程线程和GO通道的同步异步模式

分类于 后端开发 标签 chan 通道 发表于2020-05-17 17:00

在学习 goroutine 的时候,这篇学习记录就一直想写的,但是发现自己的计算机基础知识严重缺乏,好多关键词都不理解,因此花了很多时间去学习这些关键字,看来自己也是一个野路子程序员,一路走来,好多东西都是自己凭兴趣驱动的去学习的,虽然很多地方不是那么专业,但是如果用到了,就会想着去探索。所以只学自己感兴趣和用到的东西这也没什么不好的,开心最重要,不是吗?

那么,学习 goroutine 遇到第一个需要理解的就是 并发和并行,说实在的,这种看似从字面上就可以理解的东西,对于我来说,理解只能停留在表面,这可能就是自己计算机基础薄弱的问题吧。

并发 & 并行

我们可以想象一下,你去银行取钱,有2个窗口,都排了很长的队,两个窗口同时办理业务,这就是并行,但是有一个业务员临时有事,为了让这队人不重新排队,公平起见,这队人排到另外一个窗口的旁边,另外一个业务员同时处理这两队人,一边队一个人办理,切换着来,这个就是并发。

并发:逻辑上具备同时处理多个任务的能力

并行:物理上在同一个时刻执行多个并发任务

因为 cpu 执行速度极快,并发可能在微秒级别上切换任务执行,看似同时,但是并发并不是真正意义上的同时,并行就是多个任务真正在同一时刻执行,则要依赖多核处理器等物理设备。

并发就是指代码逻辑上可以并行,有并行的潜力,但是不一定当前是真的以物理并行的方式运行,并发指的是代码的性质,并行指的是物理运行状态。

当有多个线程时,如果系统只有一个CPU,那么CPU不可能真正同时进行多个线程,CPU的运行时间会被划分成若干个时间段,每个时间段分配给各个线程去执行,一个时间段里某个线程运行时,其他线程处于挂起状态,这就是并发。并发解决了程序排队等待的问题,如果一个程序发生阻塞,其他程序仍然可以正常执行。

并行是当操作系统有多个CPU时,一个CPU处理A线程,另一个CPU处理B线程,两个线程互相不抢占CPU资源,可以同时进行,这种方式成为并行。

并发只是在宏观上给人感觉有多个程序在同时运行,但在实际的单CPU系统中,每一时刻只有一个程序在运行,微观上这些程序是分时交替执行。

在多CPU系统中,将这些并发执行的程序分配到不同的CPU上处理,每个CPU用来处理一个程序,这样多个程序便可以实现同时执行。

多线程或多进程是并行的基本条件,单线程可以用协程做并发,那么问题又来了,进程,线程 , 协程又是什么??

进程 & 线程 & 协程

打开 Mac 的活动监视器,我们可以看到当前系统有都是进程,每个进程有多少线程,点击下面的 cpu 负载,可以看到我的 cpu 有8个内核。

所以,进程是“程序执行的一个实例” ,担当分配系统资源的实体,它是 资源分配的最小单位,进程创建必须分配一个完整的独立地址空间。

同一时刻执行的进程数不会超过核心数,但是单核CPU也可以运行多进程,只不过不是同时的,而是极快地在进程间来回切换实现的多进程。就像是十年前的单核CPU的电脑,也可以聊QQ的同时看视频。

而进程切换则意味着需要保留进程切换前的状态,以备切换回去的时候能够继续接着工作。所以进程拥有自己的地址空间,全局变量,文件描述符,各种硬件等等资源。操作系统通过调度CPU去执行进程的记录、回复、切换等等。

如果说进程和进程之间相当于程序与程序之间的关系,那么线程与线程之间就相当于程序内的任务和任务之间的关系。所以线程是依赖于进程的,也称为 「微进程」 。它是 程序执行过程中的最小单元 。

  1. 进程是CPU资源分配的基本单位,线程是独立运行和独立调度的基本单位(CPU上真正运行的是线程)。
  2. 进程拥有自己的资源空间,一个进程包含若干个线程,线程与CPU资源分配无关,多个线程共享同一进程内的资源。
  3. 线程的调度与切换比进程快很多。

而协程,又称微线程,纤程,英文名Coroutine,是一种用户态的轻量级线程。

阻塞 & 非阻塞

阻塞是指调用线程或者进程被操作系统挂起。 非阻塞是指调用线程或者进程不会被操作系统挂起。

同步 & 异步

同步是阻塞模式,异步是非阻塞模式。

通道

通道又叫channel,顾名思义,channel的作用就是在多个并发任务之间传递数据的。

go 的通道有同步和异步模式,其实就是有无缓冲的 channel

要理解有无缓冲我们从下面代码来理解

package main

import (
	"fmt"
)
func main() {
	done := make(chan error)
	go func() { done <- nil }()
	fmt.Println(<-done)
}

使用 go 创建并发任务之后程序会往下执行,执行到 <-done 消费时,程序会阻塞等待生产,既 done <- nil ,当生产消费之后,程序然后往下执行。

package main

import (
	"fmt"
)

func main() {
	done := make(chan error)
 //这里改成(make(chan err ,1)就能正确执行
	done <- nil
	fmt.Println(<-done)
}

这段代码会死锁,为什么呢?因为程序执行 done <- nil (生产)之后就会阻塞了,等待消费。可是下面的 done <- nil(消费) 已经执行不到了,因此会报错,done <- nil 生产1个产品, 而没有没有人消费, runtime就判定超时。但是如果把 done := make(chan error) 修改成 done := make(chan error,1)之后(有1个缓冲的通道),

这时chan能保存一个产品, 所以等东西放进去后, 后面的代码再取货,无缓冲chan是阻塞的,那make(chan error)之后立即无论是赋值还是取值都是阻塞的,无法继续执行后面的代码(死锁)。只有先新起一个goroutine,在goroutine内赋值或取值,而阻塞goroutine并不会影响main线程继续执行。在main线程内取值或赋值后,goroutine内阻塞的channel收到值后继续执行后续代码,所以相安无事

done := make(chan error, 1) //这个有缓存,为1
done <- fmt.Errorf("test") //并不会阻塞
done <- fmt.Errorf("还不死锁有鬼了") //继续执行这行代码
fmt.Println(<-done)

上面的代码也报同样的错误: fatal error: all goroutines are asleep - deadlock!

channel的机制是先进先出,如果你给channel赋值了,那么必须要读取它的值,不然就会造成阻塞,当然这个只对无缓冲的channel有效。对于有缓冲的channel,发送方会一直阻塞直到数据被拷贝到缓冲区;如果缓冲区已满,则发送方只能在接收方取走数据后才能从阻塞状态恢复。

同步模式下,生产和消费配对,然后直接复制数据给对方,如果配对失败,则置入等待队列,直到一方出现才会被唤醒。异步模式抢夺的则是数据缓冲槽,生产方要求有空槽可以写入,而消费方则要求有缓冲数据可读,需求不符时同样加入等待队列。

在某些需要频繁通讯的并发任务,加入多个缓冲可以提高程序的执行效率。

最后,go 并发怎么那么像异步呢?不过需要清楚的是,异步是实现并发的手段之一,这是俩码事,没啥比较的意义。

参考资料

go语言学习笔记

https://www.cnblogs.com/xinliangcoder/p/11286801.html

https://www.zhihu.com/question/33306646

https://zhuanlan.zhihu.com/p/70256971

https://www.cnblogs.com/shenguanpu/archive/2013/05/05/3060616.html

https://www.cnblogs.com/shenguanpu/archive/2013/05/05/3060616.html

http://blog.zhaojie.me/2013/04/why-channel-and-goroutine-in-golang-are-buildin-libraries-for-other-platforms.html