goroutine 相关知识11

历史背景

远古时代

ENIAC,它上面没有操作系统,更别提进程、线程和协程了

进程时代

  1. 单进程—串行

计算机有了操作系统,每个程序都是一个进程,但是操作系统在一段时间只能运行一个进程,直到这个进程运行完,才能运行下一个进程

单进程依然是太慢了,比如进程读数据阻塞了,CPU就在哪浪费着

  1. 多进程并发

操作系统最早的并发能力:当一个进程阻塞的时候,切换到另外等待执行的进程,尽量把CPU利用起来

线程时代

进程拥有太多资源,在创建、切换和销毁的时候,都会占用很长的时间,CPU虽然利用起来了,但CPU有很大的一部分都被用来进行进程调度了,怎么才能提高CPU的利用率?

大家希望能有一种轻量级的进程,调度不怎么花时间,这样CPU就有更多的时间用在执行任务上

操作系统支持了线程,线程在进程里面,线程运行所需要资源比进程少多了,跟进程比起来,切换简直是“不算事”

一个进程可以有多个线程,CPU在执行调度的时候切换的是线程

  • 如果下一个线程也是当前进程的,就只有线程切换,“很快”就能完成
  • 如果下一个线程不是当前的进程,就需要切换进程,这就得费点时间了

CPU调度切换的是进程和线程

多线程看起来很美好,但实际多线程编程却像一坨屎

一是由于线程的设计本身有点复杂,二是由于需要考虑很多底层细节,比如锁和冲突检测

协程

多进程、多线程已经提高了系统的并发能力,但高并发场景下,为每个任务都创建一个线程是不现实的

  • 消耗大量的内存(每个线程的内存占用级别为MB)
  • 线程多了之后调度也会消耗大量的CPU

如何充分利用CPU、内存等资源的情况下,实现更高的并发?

既然线程的资源占用、调度在高并发的情况下,依然是比较大的,是否有一种东西,更加轻量?

线程分为内核态线程和用户态线程,用户态线程需要绑定内核态线程,CPU并不能感知用户态线程的存在,它只知道它在运行1个线程,这个线程实际是内核态线程

用户态线程实际有个名字叫协程(co-routine),为了容易区分,我们使用协程指用户态线程,使用线程指内核态线程

coroutine,User-level threads, Application-level threads, Green threads都指一样的东西,就是不受OS感知的线程

Green threads的实现列表,比如Lua、Go、Erlang、Common Lisp、Haskell、Rust、PHP、Stackless Python,所以,认为用户态线程就是协程

协程跟线程是有区别的

  • 线程由CPU调度是抢占式的
  • 协程由用户态调度是协作式的,一个协程让出CPU后,才执行下一个协程

协程和线程有3种映射关系:N:1、1:1、M:N

不少语言支持了协程,比如:Lua、Erlang、就算语言不支持,也有库支持协程,比如C语言的coroutine(风云)、Kotlin的kotlinx.coroutines、Python的gevent

goroutine

就像前面说的多线程编程太不友好了,Go为了提供更容易使用的并发方法,使用了goroutine和channel

goroutine来自协程的概念,让一组可复用的函数运行在一组线程之上,即使有协程阻塞,该线程的其他协程也可以被runtime调度,转移到其他可运行的线程上

最关键的是,程序员看不到这些底层的细节,降低了编程的难度,提供了更容易的并发

Go语言的老调度器

M:N类型,用了4年左右就被替换掉了

M想要执行、放回G都必须访问全局G队列,并且M有多个,即多线程访问同一资源需要加锁进行保证互斥/同步,所以全局G队列是有互斥锁进行保护的。

老调度器有4个缺点:

  • 创建、销毁、调度G都需要每个M获取锁,这就形成了激烈的锁竞争
  • M转移G会造成延迟和额外的系统负载。比如当G中包含创建新协程的时候,M创建了G’,为了继续执行G,需要把G’交给M’执行,也造成了很差的局部性,因为G’和G是相关的,最好放在M上执行,而不是其他M'
  • M中的mcache是用来存放小对象的,mcache和栈都和M关联造成了大量的内存开销和差的局部性
  • 系统调用导致频繁的线程阻塞和取消阻塞操作增加了系统开销

新调度器

两大思想

  • 复用线程

协程本身就是运行在一组线程之上,不需要频繁的创建、销毁线程,而是对线程的复用

在调度器中复用线程还有2个体现:

  1. work stealing,当本线程无可运行的G时,尝试从其他线程绑定的P偷取G,而不是销毁线程
  2. hand off,当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行
  • 利用并行

GOMAXPROCS设置P的数量,当GOMAXPROCS大于1时,就最多有GOMAXPROCS个线程处于运行状态,这些线程可能分布在多个CPU核上同时运行,使得并发利用并行。另外,GOMAXPROCS也限制了并发的程度,比如GOMAXPROCS = 核数/2,则最多利用了一半的CPU核进行并行

两小策略

  • 抢占

在coroutine中要等待一个协程主动让出CPU才执行下一个协程,在Go中,一个goroutine最多占用CPU 10ms,防止其他goroutine被饿死,这就是goroutine不同于coroutine的一个地方

  • 全局G队列

在新的调度器中依然有全局G队列,但功能已经被弱化了,当M执行work stealing从其他P偷不到G时,它可以从全局G队列获取G

上面提到并行了,关于并发和并行再说一下:Go创始人Rob Pike一直在强调go是并发,不是并行,因为Go做的是在一段时间内完成几十万、甚至几百万的工作,而不是同一时间同时在做大量的工作。并发可以利用并行提高效率,调度器是有并行设计的。

并行依赖多核技术,每个核上在某个时间只能执行一个线程,当我们的CPU有8个核时,我们能同时执行8个线程,这就是并行。

转载于:https://my.oschina.net/zhangthe9/blog/3023032

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
作为Go语言初学者,以下是一些重要的知识点和建议,可以帮助你开始学习和使用Go语言: 1. 安装和设置Go环境:首先,确保在计算机上安装了Go语言的最新版本,并设置好相关的环境变量。可以从官方网站(https://golang.org)下载安装程序并按照说明进行安装。 2. 了解基本语法和数据类型:学习Go语言的基本语法,包括变量声明、函数定义、条件语句、循环语句等。掌握Go语言的基本数据类型,如整型、浮点型、字符串、布尔型等。 3. 学习函数和包:函数是Go语言的基本构建块之一。了解如何定义和调用函数,并理解函数的参数和返回值。此外,了解如何使用包(package)来组织和重用代码。 4. 并发编程:Go语言内置了强大的并发编程支持。学习使用goroutine和channel进行并发编程,以实现高效的并发处理和协作。 5. 错误处理:Go语言鼓励使用显式的错误处理机制。学习使用错误类型和错误处理函数来处理可能发生的错误,并避免潜在的错误。 6. 标准库和第三方库:探索Go语言的标准库,了解如何使用其中的功能和工具。此外,也要了解常用的第三方库,它们提供了许多有用的功能和工具,可以加快开发速度。 7. 学习常用的工具和技术:Go语言有许多强大的工具和技术可用于开发。学习使用Go工具链(如go build、go run、go test等),以及版本管理工具(如git)和构建工具(如Makefile)等。 8. 实践和项目:通过实践和参与项目,将所学的知识应用到实际中。可以尝试解决一些小型的编程问题,或者参与开源项目,以提高自己的编程能力和经验。 9. 阅读文档和教程:Go语言有丰富的文档和教程资源可供学习。阅读官方文档、博客文章、书籍和在线教程,可以帮助你更深入地理解和掌握Go语言的知识和技巧。 10. 加入社区和交流:加入Go语言的社区,与其他开发者交流和分享经验。参加本地的Go语言用户组、在线论坛或社交媒体群组,可以获取更多资源、解答问题,并与其他Go开发者建立联系。 记住,持续学习和实践是掌握任何编程语言的关键。通过不断地编写代码、阅读文档和参与项目,你将逐渐掌握和提高Go语言的技能。祝你在学习Go语言的过程中取得成功!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值