一.背景

在并发编程中进程和线程是不可忽略的两个概念,他们很好的完成了操作系统或者服务对于高并发的需求,然而随着时代的进步,协程的概念应运而生,本文旨在解释协程相对于进程和线程在高并发环境下的优势,所以会先介绍进程,线程,最后讲解协程的调度方式。

二.详细介绍

2.1 进程

2.1.1 概念

进程基本上是一个正在执行的程序,它是操作系统中最小的资源分配单位。

2.1.2 结构

当一个程序被加载到内存中并成为一个进程时,它可以分为四个部分——堆栈、堆、文本和数据。下图显示了主内存中进程的简化布局:

堆栈

进程堆栈包含临时数据,例如方法/函数参数、返回地址和局部变量。

这是在进程运行时动态分配给进程的内存。

数据

包含全局变量和静态变量。

文本

包括由程序计数器的值和处理器寄存器的内容表示的当前活动。

2.1.3 进程上下文切换

进程的上下文切换是指 cpu 从一个进程切换到另一个进程。

进程上下文切换主要包含两个主要过程:进程地址空间切换和处理器状态切换

进程地址空间切换

切换原因:进程地址空间指的是进程所拥有的虚拟地址空间,而这个地址空间是假的,是 linux 内核通过数据结构来描述出来的,从而使得每一个进程都感觉到自己拥有整个内存的假象,cpu 访问的指令和数据最终会落实到实际的物理地址,对用进程而言通过缺页异常来分配和建立页表映射。进程地址空间内有进程运行的指令和数据,因此到调度器从其他进程重新切换到我的时候,为了保证当前进程访问的虚拟地址是自己的必须切换地址空间。

切换方式:将当前进程的 pgd 虚拟地址转换为物理地址存放在用户控件的页表基址寄存器,当访问用户空间地址的时候 mmu 会通过这个寄存器做遍历页表,获得物理地址。

原理是进程想要访问一个用户空间虚拟地址,cpu 的 mmu 所做的工作,就是从页表基址寄存器拿到页全局目录的物理基地址,然后和虚拟地址配合来查查找页表,最终找到物理地址进行访问(当然如果 tlb 命中就不需要遍历页表),每次用户虚拟地址访问的时候(内核空间共享不考虑),由于页表基地址寄存器内存放的是当前执行进程的页全局目录的物理地址,所以访问自己的一套页表,拿到的是属于自己的物理地址(实际上,进程是访问虚拟地址空间的指令数据的时候不断发生缺页异常,然后缺页异常处理程序为进程分配实际的物理页,然后将页帧号和页表属性填入自己的页表条目中),就不会访问其他进程的指令和数据,这也是为何多个进程可以访问相同的虚拟地址而不会出现差错的原因

ps:地址空间切换过程中,还会清空 tlb(页表缓存:用于存放虚拟地址映射至物理地址的标签页表条目),防止当前进程虚拟地址转化过程中命中上一个进程的 tlb 表项,一般会将所有的 tlb 无效,但是这会导致很大的性能损失,因为新进程被切换进来的时候面对的是全新的空的 tlb,造成很大概率的 tlb miss,需要重新遍历多级页表

处理器状态切换

切换原因:需要将进程的内核栈和执行流进行切换。

切换方式:处理器状态切换就是将前一个进程的 sp,pc 等寄存器的值保存到一块内存上,然后将即将执行的进程的 sp,pc 等寄存器的值从另一块内存中恢复到相应寄存器中,恢复 sp 完成了进程内核栈的切换,恢复 pc 完成了指令执行流的切换。

  • sp 寄存器在任意时刻会保存我们栈顶的地址.

  • pc 寄存器也称为程序寄存器,用于存储指向下一条指令的地址,也即将将要执行的指令代码。

2.2 线程

2.2.1 概念

线程是进程的子集,也称为轻量级进程。一个进程可以有多个线程,这些线程由调度器独立管理。一个进程内的所有线程都是相互关联的。线程是操作系统中最小的调度单位。

2.2.2 结构

线程有一些公共信息,例如数据段、代码段、文件等,这些信息共享给它们的对等线程。但包含自己的寄存器堆栈程序计数器

堆栈:函数在被执行的时候产生的数据包括 函数参数、 局部变量、 返回地址等信息,这些信息是保存在栈中的,线程相当于进程中的一个执行流,为了保存执行流的信息,我们需要给线程创建独属堆栈

寄存器:函数运行需要额外的寄存器来保留一些信息,所以线程的寄存器也是私有的。

程序计数器:CPU 执行指令的信息保存在一个叫做程序计数器的寄存器中,通过这个寄存器我们就知道接下来要执行哪一条指令。所以线程也有自己的计数器用于告诉我们线程执行的工作顺序。

2.2.3 线程上下文切换

根据线程的结构可知,线程没有自己的地址空间,同一进程的线程之间切换,他们共享同一进程的地址空间,所以只需要切换处理器状态;不同进程的线程之间切换,会引起进程切换

由于同一进程下的线程上下文切换不引起虚拟地址空间切换,所以它们上下文切换的花销要比进程小很多。

2.3 协程

2.3.1 概念

可以看作轻量级线程,他的内存占用少只要 2k,且上下文切换成本低,是一个独立执行的函数,由 go 语言启动,由 Go 运行时(runtime)管理。Go 程序会智能地将 goroutine 中的任务合理地分配给每个 CPU。

2.3.2 结构

在 Go 中,goroutine 只不过是一个 Go 结构,包含有关正在运行的程序的信息,例如堆栈、程序计数器或其当前的 OS 线程。

type g struct { stack       stack   // offset known to runtime/cgo stackguard0 uintptr // offset known to liblink stackguard1 uintptr // offset known to liblink _panic    *_panic // innermost panic - offset known to liblink _defer    *_defer // innermost defer   m         *m      // current m; offset known to arm liblink    sched     gobuf    syscallsp uintptr // if status==Gsyscall, syscallsp = sched.sp to use during gc syscallpc uintptr // if status==Gsyscall, syscallpc = sched.pc to use during gc stktopsp  uintptr // expected sp at top of stack, to check in traceback    param        unsafe.Pointer    atomicstatus uint32    stackLock    uint32 // sigprof/scang lock; TODO: fold in to atomicstatus   goid         int64 schedlink    guintptr  waitsince    int64      // approx time when the g become blocked   waitreason   waitReason // if status==Gwaiting
  preempt       bool // preemption signal, duplicates stackguard0 = stackpreempt    preemptStop   bool // transition to _Gpreempted on preemption; otherwise, just deschedule  preemptShrink bool // shrink stack at synchronous safe point   asyncSafePoint bool
  paniconfault bool // panic (instead of crash) on unexpected fault address  gcscandone   bool // g has scanned stack; protected by _Gscan bit in status    throwsplit   bool // must not split stack  activeStackChans bool  parkingOnChan uint8    raceignore     int8     // ignore race detection events    sysblocktraced bool     // StartTrace has emitted EvGoInSyscall about this goroutine   tracking       bool     // whether we're tracking this G for sched latency statistics trackingSeq    uint8    // used to decide whether to track this G  runnableStamp  int64    // timestamp of when the G last became runnable, only used when tracking   runnableTime   int64    // the amount of time spent runnable, cleared when running, only used when tracking    sysexitticks   int64    // cputicks when syscall has returned (for tracing)    traceseq       uint64   // trace event sequencer   tracelastp     puintptr // last P emitted an event for this goroutine  lockedm        muintptr    sig            uint32  writebuf       []byte  sigcode0       uintptr sigcode1       uintptr sigpc          uintptr gopc           uintptr         // pc of go statement that created this goroutine   ancestors      *[]ancestorInfo // ancestor information goroutine(s) that created this goroutine (only used if debug.tracebackancestors)    startpc        uintptr         // pc of goroutine function racectx        uintptr waiting        *sudog         // sudog structures this g is waiting on (that have a valid elem ptr); in lock order cgoCtxt        []uintptr      // cgo traceback context labels         unsafe.Pointer // profiler labels   timer          *timer         // cached timer for time.Sleep   selectDone     uint32         // are we participating in a select and did someone win the race?    gcAssistBytes int64}

复制代码

2.3.3 协程上下文切换

goroutine 调度概念介绍

当你的 go 程序启动,他会根据你的主机分配逻辑处理器(P),每个物理核心可能有多个硬件线程,以我的电脑为例子,显示是 6 核但是可以出实话出来 12 个逻辑处理器,他们可用于并行执行 OS 线程。

每个逻辑处理器都会分配一个 OS 线程(M),该线程由操作系统管理,当 go 执行,有 12 个线程可用于执行工作,每个线程连接到一个 P。

每个 go 程序都会有一个初始的协程(G:goroutine),他可以被看作是程序级别线程,所有的 goroutine 在 M 上进行上下文切换。

最后还需要有运行队列,全局运行队列(GRQ)和本地运行队列(LRQ)。每个 P 都有一个 LRQ,用于管理分配在 P 上下文中执行的 Goroutine。这些 Goroutine 轮流在分配给该 P 的 M 上进行上下文切换。GRQ 用于管理尚未分配给 P 的 Goroutine。有一个将 Goroutines 从 GRQ 移动到 LRQ 的过程,我们将在后面讨论。

进程,线程的切换,都是操作系统进行调度的,go 调度是 go 语音的一部分,它运行在内核之上的用户空间中。它不是抢占式,而是协作调度。作为协作调度程序意味着调度程序需要在代码中的安全点发生的明确定义的用户空间事件来做出调度决策。

以上是 goroutine 的定义,从上文可知,goroutine 调度与进程线程最大的区别就在于它是运行在用户空间中的协作调度方式的上下文切换。

会触发调度程序调度决策的场景

1.go 关键字使用

2.垃圾回收

3.系统调用

4.mutex,channel 调用导致 goroutine 阻塞

切换开销

Goroutine 上下文切换只涉及修改三个寄存器(PC[程序寄存器]/SP[堆栈指针]/DX)的值,而比较线程的上下文切换需要包括模式切换(从用户态切换到内核态)和 16 个寄存器,PC、SP 等寄存器刷新

三.总结

进程上下文切换开销:

1.地址空间

2.硬件上下文

线程上下文切换开销:

1.硬件上下文

2.同一进程下不切换地址空间

goroutine 切换开销:

1.用户态,不用象线程和进程一样多进行一次内核用户态切换

2.只需要保存/恢复三个寄存器的值,开销远远小于线程

其余优点:goroutine 的栈空间为 2k,线程为 2m,进程是 10m

由进程,线程,goroutine 的上下文切换可以明显看出是一个逐步减负的过程,这个过程可以结合它们的结构来理解,coverco 故而自带 goroutine 的 go 语言在高并发开发中有着得天独厚的优势。

参考文章

https://www.toptal.com/software/introduction-to-concurrent-programming

进程:

https://www.tutorialspoint.com/operating_system/os_processes.htm

https://www.guru99.com/process-management-pcb.html

https://blog.csdn.net/21cnbao/article/details/108860584

线程

https://www.javatpoint.com/process-vs-thread

https://blog.csdn.net/weixin_39630048/article/details/113328415

协程

https://www.youtube.com/watch?v=f6kdp27TYZs

https://talks.golang.org/2012/concurrency.slide#13(关于并发演讲)

https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part2.html

https://morioh.com/p/36af32e3f52c

golang 解析 --- 进程,线程,协程相关推荐

  1. linux的进程/线程/协程系列5:协程的发展复兴与实现现状

    协程的发展复兴与实现现状 前言 本篇摘要: 1. 协同制的发展史 1.1 协同工作制的提出 1.2 自顶向下,无需协同 1.3 协同式思想的应用 2. 协程的复兴 2.1 高并发带来的问题 2.2 制 ...

  2. 简要说明__python3中的进程/线程/协程

    多任务可以充分利用系统资源,极大提升程序运行效率,多任务的实现往往与 多线程,多进程,多协程有关 稳定性: 进程 > 线程 > 协程 系统资源占用量:进程 > 线程 > 协程 ...

  3. Linux的进程/线程/协程系列4:进程知识深入总结:上篇

    Linux的进程/线程/协程系列4:进程/线程相关知识总结 前言 本篇摘要: 1. 进程基础知识 1.1 串行/并行与并发 1.2 临界资源与共享资源 1.3 同步/异步与互斥 1.4 进程控制原语 ...

  4. linux的进程/线程/协程系列3:查看linux内核源码——vim+ctags/find+grep

    linux的进程/线程/协程系列3:查看linux内核源码--vim+ctags/find+grep 前言 摘要: 1. 下载linux内核源码 2. 打标签方法:vim+ctags 2.1 安装vi ...

  5. linux的进程/线程/协程系列1:进程到协程的演化

    linux的进程/线程/协程系列1:进程到协程的演化 前言 摘要: 1. 一些历史:批处理时代 2. 现代操作系统启动过程 3. 进程(process)的出现 4. 线程(thread)与线程池 5. ...

  6. Python之进程+线程+协程(异步、selectors模块、阻塞、非阻塞IO)

    文章目录 一.IO多路复用 二.selectors模块 本篇文字是关于IO多路复用的更深入一步的总结,上一篇 Python之进程+线程+协程(事件驱动模型.IO多路复用.select与epoll)对I ...

  7. 进程 线程 协程 各自的概念以及三者的对比分析

    文章目录 1 进程 2 线程 3 进程和线程的区别和联系 3.1 区别 3.2 联系 4 举例说明进程和线程的区别 5 进程/线程之间的亲缘性 6 协程 线程(执行一个函数)和协程的区别和联系 协程和 ...

  8. 面试官:换人!他连进程线程协程这几个特点都说不出

    前言 很早之前就在构思这篇文章的主题,进程线程可以说是操作系统基础,看过很多关于这方面知识的文章都是纯理论讲述,编程新手有些难以直接服用. 于是写下这篇文章,用图解的形式带你学习和掌握进程.线程.协程 ...

  9. python 进程 线程 协程

    并发与并行:并行是指两个或者多个事件在同一时刻发生:而并发是指两个或多个事件在同一时间间隔内发生.在单核CPU下的多线程其实都只是并发,不是并行. 进程是系统资源分配的最小单位,进程的出现是为了更好的 ...

  10. 进程 线程 协程_进程,线程,协程那些事

    无论我们写出怎样的程序,最后都是由操作系统来运行我们的程序,而操作系统如何管理我们的程序,我们程序的数据如何保存和计算,这些都是操作系统需要处理的事情,我们只要将写好的程序交给操作系统就好. 虽然操作 ...

最新文章

  1. iOS app性能优化的那些事
  2. Hyperledger Fabric 词汇表
  3. liblapack.so.3: undefined symbol: gotoblas错误及解决办法
  4. 修改 MySQL 自增ID的起始值
  5. java中日期计算2月份_计算两日期间2月29日总数的Java程序
  6. 2.2 基本算法之递归和自调用函数 8758 2的幂次方表示 python
  7. Stream流的使用函数式接口
  8. echarts数据可视化系列:柱状图
  9. 网络爬虫/数据抓取,反爬虫(更新版)
  10. android 删除图片后通知系统图库删除图片
  11. Day 5 E. Arranging The Sheep
  12. css 长单词不换行溢出容器的解决方法 word-wrap与word-break
  13. 计算机运用基础2020四川传媒学院考题,2020年计算机基础考试题EY[含答案](15页)-原创力文档...
  14. 用vlookup在excel表格里查找数据
  15. 手机android版本2.3.6可以安装哪个版本的音乐播放器,喜马拉雅fm老版本2.3.6下载...
  16. 抢红包小程序(Java、头歌实验)
  17. 防病毒Clamav使用及API调用测试
  18. 用旧弃的Android智能手机变为电脑WIFI摄像头
  19. [RK3399][Android7.1]修改系统配置同时输出Speaker和USB音频
  20. 链接分析算法在网络舆情热点

热门文章

  1. 3D角色硬表面建模技巧与思路分享
  2. GEC6818 移植 rtl8723bu wifi驱动
  3. iphone icloud无法载入储存空间信息
  4. Oracle计算两个日期的月份
  5. echarts自定义地图总结(VUE)
  6. 情人节适合送礼的数码好物有哪些?心意满满的数码好物清单
  7. DNS域名服务之:排查DNS的故障
  8. 【Vue 基础知识】keep-alive是什么?怎么用?
  9. android 应用图标替换后手机安装还显示旧的图标或者显示android小人人
  10. Type-C扩展坞支持的手机类型