• 【Golang】golang并发模型


    Golang调度器

      先看看golang调度的由来。

      一. 单进程时代不需要调度器

         在早期操作系统是单进程的,一个进程拥有整个系统的所有资源,所以也不需要调度器。

      

     但是单进程的操作系统也有明显的缺点:

       1. 采用单一的执行流程,计算机只能一个任务一个任务处理。

       2. 进程阻塞所造成CPU资源的浪费。

    那么如何充分利用资源,可以让多个进程同时并发的去执行呢?
      所以后来操作系统就具有了最早的并发能力:多进程并发,当一个进程阻塞的时候,切换到另外等待执行的进程,这样就能尽量把CPU利用起来,CPU就不浪费了。
     
     二. 多进程/线程操作系统
       在支持多进程和多线程的操作系统中,可以同时运行多个进程或者多个线程,CPU采用一定的策略,比如轮训去执行多个进程或者线程,执行第一个进程然后在执行下一个进程,当一个进程阻塞CPU可以立刻切换到其他进程中去执行,而且调度CPU的策略可以保证在每个运行的进程/线程都可以被分配到CPU的运行时间片,这个时间片就是规定每个进程当前所运行的时间,当一个进程的时间片结束后,CPU就将当前进程上下文保存再去执行下一个进程,该进程下一次运行只能等到再次被分配到时间片在接着运行,因为每个时间片时间很短,这样从宏观的角度来看,似乎多个进程/线程是在同时被运行。
      
      进程/线程切换的缺点
      但是有新的问题,就是当某个执行进程拥有太多的资源,进程的创建、切换、销毁,都会占用很长的时间,CPU虽然利用起来了,但如果进程过多,CPU有很大的一部分都被用来进行进程调度了,比如进程的上下文切换,就要保存当前进程的信息,这就需要系统调用,以及拷贝/复制等这些操作,所以进程/线程数量越多,切换的成本就越大(尤其是进程),CPU要处理切换这些事情,造成对CPU的利用率就越低,所以对性能就越有影响。
     
       

      

      进程比较重,占用资源比较大,比较占用内存空间,CPU切换必然对性能有很大的影响。

      相对于进程,线程虽然比进程轻量,也被称为轻量级进程(Lightweight Process,LWP),但是实际上多线程开发设计会变得更加复杂,要考虑很多同步竞争等问题,如锁、竞争冲突等,开发也变得非常复杂。

      

     那么怎么才能提高CPU的利用率呢?

      三. 使用协程来提高CPU利用率
      协程(coroutine)被称为是一种用户态的轻量级线程,协程也是一种线程,而一个线程分为"内核态"的线程和"用户态"的线程,一个“用户态线程”必须要绑定一个“内核态线程”,但是CPU并不知道有“用户态线程”的存在,CPU只知道它运行的是一个“内核态线程”(Linux的PCB进程控制块),如下图所示。
      

       在Go语言中,协程(coroutine)是Go语言中的轻量级线程实现,那么我们可以内核线程依然叫 “线程 (thread)”,而用户线程叫 “协程 (co-routine)”,如下。

      

     上图里一个协程 (co-routine) 可以绑定一个线程 (thread),这种是一个协程对应一个线程,所以这种是1:1模型。协程的创建,删除,切换也都是需要CPU去完成的。

     当然多个协程 (co-routine) 也可以绑定一个或者多个线程 (thread),这样就是N个协程对应一个线程 。

     

     上图三个协程在用户态,对应内核空间的一个线程,用户态重的协程在工作时候也是可以进行切换的,但是这种切换是在用户态,协程之间的切换是由图中协程调度器去完成的,这样带来的好处就是协程在用户态线程即完成切换,不会陷入到内核态,这种切换非常的轻量快速

      但是N:1这种模型也是有缺点的

      N:1模型的缺点:

    • 某个程序用不了硬件的多核加速能力。
    • 一旦某协程阻塞,造成线程阻塞,本进程的其他协程都无法执行了,根本就没有并发的能力了。

    进一步改良M:N模型,就是M个协程对应N个线程,如下图所示。

     上图内核态有两个线程,当有两个CPU的时候,就可以利用多核,每个CPU可以绑定一个线程,不过这种处理模式就更加的复杂了,线程和协程之间都是通过协程调度器去协作和管理,所以调度器的性能就显得非常重要。

    四. Go语言中的协程goroutine

       在Go语言中,协程被称为goroutine,它非常轻量,一个goroutine只占几KB,这个比线程轻量一个数量级,因为占用内存小,所以调度更灵活 (runtime 调度),切换也可以很频繁。无论协程还是线程都需要调度器去完成调度,我们需要了解最关键的调度协程的调度器的实现原理。

      来看看早期Go语言的goroutine调度器如何实现的。

      

      在早期goroutine调度器中,每创建一个协程goroutine就会被添加到一个全局go协程队列中,当线程M0想要获取一个goroutine时候,就去从全局go协程队列中获取,全局go协程队列会有一个锁来保护,当线程获取锁之后就从全局队列中拿到一个goroutine并去执行,执行后就去将锁换回去,并将goroutine放回,这就是一个完整的过程。

      这种调度器比较简单,但是也是有缺点的

      缺点:

        1. 创建,销毁,调度goroutine需要每个线程都先获得锁,这样就容易形成锁竞争。

        2. 线程转移goroutine会造成延迟和额外的系统负载。比如线程(M1)当执行中的goroutine(G1)中包含创建新协程的时候,线程(M1)创建新的一个goroutine(G2),为了继续执行这个新的goroutine(G2),需要把这个新的goroutine(G2)交给另一个线程(M2)执行,也造成了很差的局部性,因为 (G2)和(G1)是相关的,最好放在(M1)上执行,而不是其他(M2)。

        3. 系统调用 (CPU 在 M 之间的切换) 导致频繁的线程阻塞和取消阻塞操作增加了系统开销。

    CSP模型介绍

      CSP模型的全称是Communicating Sequential Process,翻译是通信顺序进程,是一种并发编程模型,还有一种并发模型是Actor模式,著名的并发编程语言Erlang就是用的Actor模式csp模型在上个世纪70年代就提出来了,是用于描述两个独立的并发实体通过共享的通讯channel(也就是管道)来进行通信的并发模型。相对于Actor模型来说,CSP中的channel是第一类对象,它不关注发送消息的实体,而关注与发送消息时候所使用的channel,CSP模型是一种很强大的并发通讯模型,也成为面向并发编程语言的理论源头,也就诞生出后来的golang等语言。

       对于golang来说,其实只用到了CSP模型的很小一部分,即Process/Channel,对应golang语言就是goroutine/channel,这两个并发关键字之间没有从属关系,Process可以是一个进程,线程,甚至可以是一个代码块,Process可以订阅任意个Channel,Channel也可不关心是从那个Process在利用它通信,Process围绕Channel进行读写。

      goroutine 是通过 GMP 调度模型实现的。
      
      G: 表示一个 goroutine,它有自己的栈。

           M: 表示内核级线程,一个 M 就是一个线程,goroutine 跑在 M 之上的。
      P: 全称是 Processor,处理器。它主要用来执行 goroutine 的,同时它也维护了一个 goroutine 队列。

    参考来源

      https://www.bilibili.com/read/cv5098443

      https://morsmachine.dk/go-scheduler

  • 相关阅读:
    jquery封装常用方法
    附加到iis进程调试时找不到w3wp.exe
    mongodb与sql语句对照表
    leetcode第一刷_Convert Sorted Array to Binary Search Tree
    swift 拼图小游戏
    散列技术之链地址法(基于无序链表)
    hdu 4850 字符串构造---欧拉回路构造序列 递归+非递归实现
    xtrabackup 2.3.3编译安装
    Android 绘制圆形图片
    赫夫曼树的构建、编码、译码解析
  • 原文地址:https://www.cnblogs.com/songgj/p/9260600.html
Copyright © 2020-2023  润新知