写了一年golang,来聊聊进程、线程与协程

捉虫大师 等级 679 0 0

本文已收录 https://github.com/lkxiaolou/lkxiaolou 欢迎star。

进程

在早期的单任务计算机中,用户一次只能提交一个作业,独享系统的全部资源,同时也只能干一件事情。进行计算时不能进行 IO 读写,但 CPU 与 IO 的速度存在巨大差异,一个作业在 CPU 上所花费的时间非常少,大部分时间在等待 IO。

为了更合理的利用 CPU 资源,把内存划分为多块,不同程序使用各自的内存空间互不干扰,这里单独的程序就是一个进程,CPU 可以在多个进程之间切换执行,让 CPU 的利用率变高。

为了实现 CPU 在多个进程之间切换,需要保存进程的上下文(如程序计数器、栈、内核数据结构等等),以便下次切换回来可以恢复执行。还需要一种调度算法,Linux 中采用了基于时间片和优先级的完全公平调度算法。

线程

多进程的出现是为了解决 CPU 利用率的问题,那为什么还需要线程?答案是为了减少上下文切换时的开销

进程在如下两个时间点可能会让出 CPU,进行 CPU 切换:

  • 进程阻塞,如网络阻塞、代码层面的阻塞(锁、sleep等)、系统调用等
  • 进程时间片用完,让出 CPU

而进程切换 CPU 时需要进行这两步:

  • 切换页目录以使用新的地址空间
  • 切换内核栈和硬件上下文

进程和线程在 Linux 中没有本质区别,他们最大的不同就是进程有自己独立的内存空间,而线程(同进程中)是共享内存空间。

在进程切换时需要转换内存地址空间,而线程切换没有这个动作,所以线程切换比进程切换代价更小。

为什么内存地址空间转换这么慢?Linux 实现中,每个进程的地址空间都是虚拟的,虚拟地址空间转换到物理地址空间需要查页表,这个查询是很慢的过程,因此会用一种叫做 TLB 的 cache 来加速,当进程切换后,TLB 也随之失效了,所以会变慢。

综上,线程是为了降低进程切换过程中的开销。

协程

当我们的程序是 IO 密集型时(如 web 服务器、网关等),为了追求高吞吐,有两种思路:

  1. 为每个请求开一个线程处理,为了降低线程的创建开销,可以使用线程池技术,理论上线程池越大,则吞吐越高,但线程池越大,CPU 花在切换上的开销也越大

线程的创建、销毁都需要调用系统调用,每次请求都创建,高并发下开销就显得很大,而且线程占用内存是 MB 级别,数量不能太多

为什么线程越多 cpu 切换越多?准确来说是可执行的线程越多,cpu 切换越多,因为操作系统的调度要保证绝对公平,有可执行线程时,一定是要雨露均沾,所以切换次数变多

  1. 使用异步非阻塞的开发模型,用一个进程或线程接收请求,然后通过 IO 多路复用让进程或线程不阻塞,省去上下文切换的开销

这两个方案,优缺点都很明显,方案1实现简单,但性能不高;方案2性能非常好,但实现起来复杂。有没有介于这两者之间的方案?既要简单,又要性能高,协程就解决了这个问题。

协程是用户视角的一种抽象,操作系统并没有这个概念,其主要思想是在用户态实现调度算法,用少量线程完成大量任务的调度。

协程需要解决线程遇到的几个问题:

  • 内存占用要小,且创建开销要小
  • 减少上下文切换的开销

第一点好实现,用户态的协程,只是一个数据结构,无需系统调用,而且可以设计的很小,达到 KB 级别。

第二点只能减少上下文切换次数来解决,因为协程的本质还是线程,其切换开销在用户态是无法降低的,只能通过降低切换次数来达到总体上开销的减少,可以有如下手段:

  1. 让可执行的线程尽量少,这样切换次数必然会少
  2. 让线程尽可能的处于运行状态,而不是阻塞让出时间片

Goroutine

goroutine 是 golang 实现的协程,其特点是在语言层面就支持,使用起来非常方便,它的核心是MPG调度模型:

  • M:内核线程
  • P:处理器,用来执行 goroutine,它维护了本地可运行队列
  • G:goroutine,代码和数据结构
  • S:调度器,维护M和P的信息

除此之外还有一个全局可运行队列。

写了一年golang,来聊聊进程、线程与协程

  1. 在 golang 中使用 go 关键字启动一个 goroutine,它将会被挂到 P 的 runqueue 中,等待被调度

写了一年golang,来聊聊进程、线程与协程

  1. 当 M0 中正在运行的 G0 阻塞时(如执行了一个系统调用),此时 M0 会休眠,它将放弃挂载的 P0,以便被其他 M 调度到

写了一年golang,来聊聊进程、线程与协程

  1. 当 M0 系统调用结束后,会尝试“偷”一个 P,如果不成功,M0 将 G0 放到全局的 runqueue 中

  2. P 会定期检查全局 runqueue,保证自己消化完 G 后有事可做,同时也会从其他 P 里“偷” G

从上述看来,MPG 模型似乎只限制了同时运行的线程数,但上下文切换只发生在可运行的线程上,应该是有一定的作用,当然这只是一部分。

golang 在 runtime 层面拦截了可能导致线程阻塞的情况,并针对性优化,他们可分为两类:

  • 网络 IO、channel 操作、锁:只阻塞 G,M、P 可用,即线程不会让出时间片
  • 系统调用:阻塞 M,P 需要切换,线程会让出时间片

所以综合来看,goroutine 会比线程切换开销少。

总结

从单进程到多进程提高了 CPU 利用率;从进程到线程,降低了上下文切换的开销;从线程到协程,进一步降低了上下文切换的开销,使得高并发的服务可以使用简单的代码写出来,技术的每一步发展都是为了解决实际问题。


搜索关注微信公众号"捉虫大师",后端技术分享,架构设计、性能优化、源码阅读、问题排查、踩坑实践。

写了一年golang,来聊聊进程、线程与协程

收藏
评论区

相关推荐

Go Context 并发编程简明教程
1 为什么需要 Context WaitGroup 和信道(channel)是常见的 2 种并发控制的方式。 如果并发启动了多个子协程,需要等待所有的子协程完成任务,WaitGroup 非常适合于这类场景,例如下面的例子: var wg sync.WaitGroup func doTask(n int) { time.Sleep(time.Durat
golang 分析调试高阶技巧
layout: post title: “golang 调试高阶技巧” date: 2020603 1:44:09 0800 categories: golang GC 垃圾回收 golang 高阶调试 Golang tools nm compile
Golang根据URL判断媒体协议
目录问题解决 问题如何根据一个流媒体地址URL判断对应的流媒体协议,比如RTMP、RTSP协议等。 解决这里提供一个方法,可以直接拿来用。 golang func getProtocol(url string) (string, error) { if url "" { index : strings.Index(url, ":")
盘点golang中的开发神器
本文已收录 https://github.com/lkxiaolou/lkxiaolou 欢迎star。在Java中,我们用Junit做单元测试,用JMH做性能基准测试(benchmark),用asyncprofiler剖析cpu性能,用jstack、jmap、arthas等来排查问题。作为一名比较新的编程语言,golang的这些工具是否更加好用呢? 单元测
写了一年golang,来聊聊进程、线程与协程
本文已收录 https://github.com/lkxiaolou/lkxiaolou 欢迎star。 进程在早期的单任务计算机中,用户一次只能提交一个作业,独享系统的全部资源,同时也只能干一件事情。进行计算时不能进行 IO 读写,但 CPU 与 IO 的速度存在巨大差异,一个作业在 CPU 上所花费的时间非常少,大部分时间在等待 IO。为了更合理的利用
Go WEB入门
摘要 由于Golang优秀的并发处理,很多公司使用Golang编写微服务。对于Golang来说,只需要短短几行代码就可以实现一个简单的Http服务器。加上Golang的协程,这个服务器可以拥有极高的性能。然而,正是因为代码过于简单,我们才应该去研究他的底层实现,做到会用,也知道为什么这么用。 在本文中,会以自顶向下的方式,从如何使用,到如何实现,一点点的分
go 协程
package utils import ( "bytes" "fmt" "runtime" "strconv" ) _/\*__获取协程__ID\*/_ func GetGoroutineID() { b := make(\[\]byte, 64) b \= b\[:runtime.Stack(b, false)\] b \=
Golang In PingCAP
随着 Golang 在后端领域越来越流行,有越来越多的公司选择 Golang 作为主力开发语言。本次 GopherChina Beijing 2016 大会上,看到 Golang 在各家公司从人工智能到自动运维,从 Web 应用到基础架构都发挥着越来越多的作用。可以说 Golang 在这几年间,获得了长足的进步。 PingCAP 是一家由几名 Go
Golang 开发环境搭建
Golang 是 Google 发布的开发语言,Go 编译的程序速度可以媲美 C/C++。 安装 -- sudo apt-get install golang sudo apt-get install golang-go.tools 使用 -- * 编译运行程序 go run main.go * 查看命令文
Golang 微框架 Gin 简介
### 所谓框架 框架一直是敏捷开发中的利器,能让开发者很快的上手并做出应用,甚至有的时候,脱离了框架,一些开发者都不会写程序了。成长总不会一蹴而就,从写出程序获取成就感,再到精通框架,快速构造应用,当这些方面都得心应手的时候,可以尝试改造一些框架,或是自己创造一个。 曾经我以为Python世界里的框架已经够多了,后来发现相比golang简直小巫见大巫。
Goroutine并发调度模型深度解析之手撸一个协程池
[golang](https://www.oschina.net/action/GoToLink?url=http%3A%2F%2Fblog.taohuawu.club%2Ftag%2Fgolang)[goroutine](https://www.oschina.net/action/GoToLink?url=http%3A%2F%2Fblog.taohua
Go实现FastCgi Proxy Client 系列(三)优化篇
墨迹一点 ==== #### 个人琐碎 最近比较忙,以致于很久都没有写blog了,但是,golang的水平自认为是总算入门了。 #### 协程的个人理解 网上的说法一般都是协程是轻量级线程。 我个人认为协程的好处 1. 小 2. 无需在用户态和内核态切换(完全在用户态) 3. 无需线程上下文切换的开销(因为之上的好处) 4. 编码简单(原
Python 协程与 Go 协程的区别(二)
👆 “Python猫” ,一个值得加星标的公众号 **花下猫语:** 今天继续分享协程系列的第二篇。万字长文,收藏起来慢慢读吧。PS:原文中有挺多参考链接,微信不能很好保留。故建议阅读原文。 作者:lgj\_bky(经作者授权转载) 原文: https://www.cnblogs.com/lgjbky/p/10838035.html 写在前面
Python进程、线程、协程的对比
### 1\. 执行过程 * 每个线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在进程中,由进程提供多个线程执行控制。每个线程都有他自己的一组CPU寄存器,称为线程的上下文,该上下文反映了线程上次运行该线程的CPU寄存器的状态。 * 协程,又称微线程,Coroutine。执行过程中,在子程序内部可中断,然后转而
Skynet 初探(1) 之 echo 复读机
    最近在关注云风大神基于C+Lua写的Skynet网络框架!采用单进程多线程的Actor并发模型,每个Actor都可以理解成一个服务(协程),服务之间的通信也是采用消息传递的机制与golang、erlang很像。     但是由于大神们的境界太高并且手册、文档较少,所以对初学者来说确实有点难入门。基本只能在目录下的examples与test目录下看实