Week3 - 你有没有想过,到底Server是如何「同时处理多个requests」的? - 行程、线程、协程篇 [No

文章也同时发表于medium(`・ω・´)”

稍微提一下,以下所有图画都是我妹妹帮忙画的,希望有帮助大家~


髒沙发LineBot在开发时曾经碰过一个问题,就是在处理大图的时候,有两个步骤,分别是

图片处理:属于CPU操作图片储存:属于IO操作

而在上篇文章有介绍Node.js对于IO操作是不会阻塞的,CPU操作是会阻塞的,这是由于Node.js是single thread,没有multi thread的帮忙来处理CPU-bound。

在Golang方面,由于goroutine拥有操作多个thread的能力,所以可以让每个thread来分工CPU操作。

Golang处理的方法遵照MPG模型来处理这类的行为,但如果直接讨论可人会让人一脸矇逼。我们可以介绍「行程到现在高併发的协程」来慢慢了解「出现什么问题,可用什么方法应对」,这样应该会比较好了解。

行程、线程、协程

我们可先有一个CPU核心运作的概念,以单纯的方式来讨论问题,「单一个CPU核心,多执行绪」的运作方式如下

单CPU核心会切成许多的时间切片(timeslicing),一下做thread A的事情,一下做thread B事情,而当做得非常快,就像用来「一个人执行多件事」,举个最近很好看的音速小子来当例子

单CPU核心就是音速小子,因为做事太快,所以同时做切菜、拖地、与蛋头博士泡茶,在一般人的感知里就像同时做这三件事情

有了这个概念后,我们就可以开始讨论

行程

行程是「资源和独立运行的最小单位」,而在以前,因为还没有「multi thread的概念」,所以行程同时也是「执行的最小单位」,这导致一个问题

在切换thread A或thread B的事情时,开销变得很大

我们称这个切换行为为context switch,当音速小子切菜时身上需要有菜刀、大葱、胡萝波,之后拖地又要準备拖把、水桶,再来要泡茶还要準备茶杯,在这每次的切换工作时也要「切换资源」,导致切换的成本变很大,那这要怎么解决呢?

于是就有了线程的概念。

线程

线程这个概念的出现取代了行程「执行的最小单位」的位置,所以要处理多件事情时,可以不需开多个行程,而是「一个行程多个线程」,即

音速小子把这三件事情所需的各个资源都带在身上,这样切换工作时就不用再拿菜刀换拖把了

协程

一切都很美好,直到「周边IO-瓦斯炉」滚茶让音速小子乾等了许久。

由于时间切片是固定的,每次做事的时间也是固定的,这导致音速小子在切换到泡茶这件事时,一直在等周边的瓦斯炉把茶滚好,所以一直看着茶发呆,这样等于白白浪费了这些时间

是否能在滚茶时,让thread的事情从滚茶换成擦茶桌呢?

可以,这时我们可以在程式码这样写

go 滚茶() //这行不会等他完成即会往下一行跑擦完茶桌后,再等茶滚好()

这样音速小子就可以更充分利用时间。

你可能有注意到,我们在程式码里面做了跟「单一个CPU核心,多执行绪」类似的行为,即是「切换事情」。

值得注意的是,

协程的切换是由程式码完成的,而单一个CPU核心线程的切换是由时间切片固定分配的。

这代表协程更加的轻量,因为比起线程它切换的开销更小了,因为它是在原thread里面做切换,不像多线程还要跨thread来切换。

多核心与单核心的差异

大家会发现,单核心始终是一个人,所以很快速切换处理事情的时间,其实同等于认真处理一件一件事情的时间

甚至在快速切换时,因为多了这个切换的开销,往往会比认真一件一件处理来得更久

所以这时就出现了Parallel(平行)这个概念,即是多核心,大家可以看到由于核心的增加可以让同一时间可以处理更多thread

有了这些概念后,欢迎来到现代世界,我们把多核心纳入讨论

多核心的世界里,需要考虑的事情比单核心来得更多,这些核心要怎么分配thread就变成了一门学问,大家要先有一个观念,就是thread分别有

kernel thread:系统层级的thread,由核心所支持,一般来说一个核心支持一个kernal thread,可操作各种底层API与接受user thread所要求要做的事情user thread:使用者层级的thread,无法直接调用底层API,都要透过kernal thread来进行调度

现在有查尔斯与音速小子两个核心,现在他们一样要做切菜、拖地、与蛋头博士泡茶这三件事情,他们一样非常快速的做这些事,但三件事情给两个人做势必会遇到「分配」上的问题,所以有以下方法来分配事情。

讲得有点抽象,以下配合几个角色,我们用图来解释

K1: 查尔斯,是kernel thread 1K2: 音速小子,是kernel thread 2U1: 切菜的事情,是user thread 1U2: 拖地的事情,是user thread 2

一对一:大家就好好做自己的事

优点:一个kernal thread就对上一件事情,意义上来说实现了真正的平行处理当事情阻塞了其中一个kernal thread,并不会导致其他kernal thread阻塞。缺点:由于一般来说一个核心支援一个kernal thread,所以三件事情事实上要再新增一个核心才可以一次处理三件当在切换事情的时候,由于是kernel thread在切换,所以开销是很大的。

一对多:把全部事情都塞给一个人做

优点:与一对一不同,由于事情是在使用者层级切换,统一由一个kernal thread处理,所以并不会被核心数限制。与一对一不同,由于事情是在使用者层级切换,不用切换kernal thread,所以相对来说快很多。缺点:如果一件事赢卡住了kernal thread,那所有事情都会被卡住。增加核心数对于系统速度几乎没有帮助,因为所有事情都交给一个人做了。

多对多:等等,全部事情大家分配着做才对吧!

优点:拥有一对一与一对多的全部优点缺点:系统变得複杂,导致user thread较难区分哪些事情是重要的,该分配给哪个kernal thread

做个统整

大家可以发现,原本的行程为什么后来需要线程、内核线程、用户线程、协程,其实最大的目标不外乎就是

让核心在切换事情的成本下降降低核心等待的情形发生,即善用核心的所有时间

所以统整下来,可以将行程、线程、协程的关係画张广义的示意图

简单的来说就是依照不同的情境,以不同的层次来分配他们事情

需换人也需换资源:那就换行程需换人但不需换资源:那就换线程不需换人也不需换资源,但不要乾等,先做其他事:那就用协程

稍微介绍完这些概念后,接下来会介绍Golang的MPG模型,谢谢你的阅读ヽ(・ω・ゞ)

如有错误欢迎勘正指教,谢谢你的阅读~


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章