Golang协程goroutine

进程,线程和协程概念

进程

  • 进程是程序在一个数据集上的一次运行过程。
  • 进程是操作系统进行资源分配的基本单位。
  • 每个进程都有自己的独立内存空间,不同进程通过进程间同步信号量来通信。
  • 由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。

线程

  • 线程是进程中的一个实体,是被操作系统进行CPU调度和执行的基本单位。
  • 一个进程包含一个或多个线程;它是比进程更小的能独立运行的基本单位.;线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器)。
  • 一个线程与同属一个进程的其他的线程共享进程所拥有的全部资源。
  • 线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。

协程

  • 协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。
  • 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

进程、线程 和 协程区别

  • 进程、线程,都是内核进行调度,CPU采用的是时间片轮换+优先级抢占式调度(有多种调度算法)
  • 协程(用户级轻量级线程),对内核透明的,也就是系统并不知道有协程的存在,是完全由用户自己的程序进行调度的,它不像抢占式调度强制 CPU 交出控制权,切换到其他进程/线程,通常只能进行协作式调度,需要协程自己主动把控制权转让出去之后,其他协程才能被执行到。

goroutine 和协程

  • 本质上,goroutine 就是协程。 不同的是,Golangruntime、系统调用等多方面对 goroutine 调度进行了封装和处理,当遇到长时间执行或者进行系统调用时,会主动把当前 goroutineCPU (P) 转让出去,让其他 goroutine 能被调度并执行,也就是 Golang 从语言层面支持了协程。
  • Golang 的一大特色就是从语言层面原生支持协程,在函数或方法前面加 go关键字就可创建一个协程。

内存消耗

每个 goroutine (协程) 默认占用内存远比 JavaC线程少。
goroutine:2KB
线程:8MB

线程和 goroutine 调度开销

线程/ goroutine 切换开销方面, goroutine 远比线程小。
线程:涉及模式切换(从用户态切换到内核态)、 16个寄存器、 PCSP(段寄存器)…等寄存器的刷新。
goroutine:只有三个 寄存器的值修改 - PC / SP / DX

AX(累加器),BX(基址寄存器),CX(计数寄存器),DX(数据寄存器),SP(堆栈指针),BP(基址指针),SI(源变址指针),DI(目的变址指针),IP(指令指针),CS(代码段寄存器),DS(数据段寄存器),SS(堆栈寄存器),ES(附加段寄存器),PC(程序计数寄存器),LR( 链接寄存器)

协程底层实现原理

线程是操作系统的内核对象,多线程编程时,如果线程数过多,就会导致频繁的上下文切换,这些 CPU时间是一个额外的耗费。所以在一些高并发的网络服务器编程中,使用一个线程服务一个 socket 连接是很不明智的。于是操作系统提供了基于事件模式的异步非阻塞编程模型。用少量的线程来服务大量的 socket网络连接和I/O操作。但是采用异步和基于事件的编程模型,复杂化了程序代码的编写,非常容易出错。因为线程穿插,也提高排查错误的难度。

协程,是在应用层模拟的线程,它避免了上下文切换的额外耗费,兼顾了多线程的优点。简化了高并发程序的复杂度。举个例子,一个高并发的网络服务器,每一个socket连接进来,服务器用一个协程来对他进行服务。代码非常清晰。而且兼顾了性能。

协程调度

协程和线程的原理是一样的,当A线程切换到B线程的时候,需要进入内核态A线程的相关执行进度压栈,然后将B线程的执行进度出栈,进入B线程的执行序列;协程只不过是在用户态实现了这一点。

Q:协程并不是由操作系统调度的,而用户态应用程序也没有能力和权限执行CPU调度。怎么解决这个问题?

  • 协程是基于线程的。内部实现上,维护了一组数据结构(P上下文环境)和N个线程(M 一个M对应一个内核级线程),真正的执行还是线程,协程(G goroutine)执行的代码被扔进N+1个待执行队列中,由这N个线程从不同的队列中调度取出来执行。这就解决了协程的执行问题。

  • Golang 对各种I/O函数 进行了封装,将这些封装的函数提供给应用程序使用,而其内部调用了操作系统的异步I/O函数,当这些异步函数返回busybloking时,Golang 利用这个时机将现有的执行序列压栈,让线程去队列中取出另外一个协程(goroutine)的代码来执行,基本原理就是这样,利用并封装了操作系统的异步函数。包括 LinuxepollselectWindowsiocpevent 等。

基于抢占式的协作调度
我们知道与用户级线程M(物理处理器,runtime运行时系统内核调度实体),会对应一个内核级线程,调度方式肯定是CPU抢占式调度。
多个G(goroutine),被挂在一个逻辑处理器P上。然后由p负责把那个G,送给M执行。
这样的设计就避免了,好多线程,切换导致cpu利用率低的问题。所有G执行都在一个M上进行,M执行等待时间大大减小。
那么问题来了goroutine执行方式不是非抢占式的吗?怎么体现啊。
Golang的运行时runtime系统,调度逻辑处理器P,协作调度G到M上运行,是非抢占式的。(它没有时间片,但是为了避免某些goroutine等时过长产生饥饿,会有超时调度,属于初级抢占式)
调度过程
Go程序的初始化,runtime创建一条后台线程,运行一个sysmon函数。这个函数会周期性地做epoll操作,同时它还会检测每个P是否运行了较长时间。
如果检测到某个P状态处于Psyscall超过了一个sysmon的时间周期(20us),并且还有其它可运行的任务,则调度切换P。
如果检测到某个P的状态为Prunning,并且它已经运行了超过10ms,则会将P的当前的G的stackguard设置为StackPreempt。相当于做了一个标记,通知这个G在合适时机进行调度。
Go使用的是分段栈,它会在每个函数入口处比较当

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值