参考:《快学 Go 语言》第 11 课 —— 千军万马跑协程 - 云+社区 - 腾讯云

协程和通道是 Go 语言作为并发编程语言最为重要的特色之一,初学者可以完全将协程理解为线程,但是用起来比线程更加简单,占用的资源也更少。通常在一个进程里启动上万个线程就已经不堪重负,但是 Go 语言允许你启动百万协程也可以轻松应付。如果把协程比喻成小岛,那通道就是岛屿之间的交流桥梁,数据搭乘通道从一个协程流转到另一个协程。通道是并发安全的数据结构,它类似于内存消息队列,允许很多的协程并发对通道进行读写。

Go 语言里面的协程称之为 goroutine,通道称之为 channel。

协程的启动

Go 语言里创建一个协程非常简单,使用 go 关键词加上一个函数调用就可以了。Go 语言会启动一个新的协程,函数调用将成为这个协程的入口。

package mainimport "fmt"
import "time"func main() {fmt.Println("run in main goroutine")go func() {fmt.Println("run in child goroutine")go func() {fmt.Println("run in grand child goroutine")go func() {fmt.Println("run in grand grand child goroutine")}()}()}()time.Sleep(time.Second)fmt.Println("main goroutine will quit")
}

输出:

run in main goroutine
run in child goroutine
run in grand child goroutine
run in grand grand child goroutine
main goroutine will quit

main 函数运行在主协程(main goroutine)里面,上面的例子中我们在主协程里面启动了一个子协程,子协程又启动了一个孙子协程,孙子协程又启动了一个曾孙子协程。这些协程之间似乎形成了父子、子孙、关系,但是实际上协程之间并不存在这么多的层级关系,在 Go 语言里只有一个主协程,其它都是它的子协程,子协程之间是平行关系

值得注意的是这里的 go 关键字语法和前面的 defer 关键字语法是一样的,它后面跟了一个匿名函数,然后还要带上一对(),表示对匿名函数的调用。

上面的代码中主协程睡眠了 1s,等待子协程们执行完毕。如果将睡眠的这行代码去掉,将会看不到子协程运行的痕迹。

package mainimport "fmt"//import "time"func main() {fmt.Println("run in main goroutine")go func() {fmt.Println("run in child goroutine")go func() {fmt.Println("run in grand child goroutine")go func() {fmt.Println("run in grand grand child goroutine")}()}()}()// time.Sleep(time.Second)fmt.Println("main goroutine will quit")
}

输出:

run in main goroutine
main goroutine will quit

这是因为主协程运行结束,其它协程就会立即消亡,不管它们是否已经开始运行

子协程异常退出

在使用子协程时一定要特别注意保护好每个子协程,确保它们正常安全的运行。因为子协程的异常退出会将异常传播到主协程,直接会导致主协程也跟着挂掉,然后整个程序就崩溃了。

package mainimport "fmt"
import "time"func main() {fmt.Println("run in main goroutine")go func() {fmt.Println("run in child goroutine")go func() {fmt.Println("run in grand child goroutine")go func() {fmt.Println("run in grand grand child goroutine")panic("wtf")}()}()}()time.Sleep(time.Second)fmt.Println("main goroutine will quit")
}

输出结果:

panic: wtfgoroutine 3 [running]:
main.main.func1.1.1()/Users/rongsong/Develop/go/my_test/go-func/go_func.go:14 +0x96
created by main.main.func1.1/Users/rongsong/Develop/go/my_test/go-func/go_func.go:12 +0x92
run in main goroutine
run in child goroutine
run in grand child goroutine
run in grand grand child goroutine

我们看到主协程最后一句打印语句没能运行就挂掉了,主协程在异常退出时会打印堆栈信息。从堆栈信息中可以了解到是哪行代码引发了程序崩溃。

为了保护子协程的安全,通常我们会在协程的入口函数开头增加 recover() 语句来恢复协程内部发生的异常,阻断它传播到主协程导致程序崩溃。

go func() {if err := recover(); err != nil {// log error}// do something
}()

比如:

package mainimport "fmt"
import "time"func main() {fmt.Println("run in main goroutine")go func() {fmt.Println("run in child goroutine")go func() {fmt.Println("run in grand child goroutine")go func() {if err := recover(); err != nil {// log errorpanic("wtf")fmt.Println("error happend in grand child")}// do somethingfmt.Println("run in grand grand child goroutine")}()}()}()time.Sleep(time.Second)fmt.Println("main goroutine will quit")
}

输出结果:

run in main goroutine
run in child goroutine
run in grand child goroutine
run in grand grand child goroutine
main goroutine will quit

启动百万协程

Go 语言能同时管理上百万的协程,这不是吹牛,下面我们就来编写代码跑一跑这百万协程,读者们请想象一下这百万大军同时奔跑的感觉。

package mainimport "fmt"
import "time"func main() {fmt.Println("run in main goroutine")i := 1for {go func() {for {time.Sleep(time.Second)}}()if i % 10000 == 0 {fmt.Printf("%d goroutine started\n", i)}i++}
}

上面的代码将会无休止地创建协程,每个协程都在睡眠,为了确保它们都是活的,协程会 1s 钟醒过来一次。在我的个人电脑上,这个程序瞬间创建了 200w 个协程,观察发现内存占用在 4G 多,这意味着每个协程的内存占用大概 2000 多字节。协程还在继续创建,电脑开始变的卡顿,应该是程序开始使用交换分区,CPU 占用率持续走高。再继续压榨下去已经没有了意义。

协程死循环

前面我们通过 recover() 函数可以防止个别协程的崩溃波及整体进程。但是如果有个别协程死循环了会导致其它协程饥饿得到不运行么?下面我们来做一个实验。

package mainimport "fmt"
import "time"func main() {fmt.Println("run in main goroutine")n := 3for i:=0; i<n; i++ {go func() {fmt.Println("dead loop goroutine start")for {}  // 死循环}()}for {time.Sleep(time.Second)fmt.Println("main goroutine running")}
}

通过调整上面代码中的变量 n 的值可以发现一个有趣的现象,当 n 值大于 3 时,主协程将没有机会得到运行,而如果 n 值为 3、2、1,主协程依然可以每秒输出一次。要解释这个现象就必须深入了解协程的运行原理。

协程的本质

一个进程内部可以运行多个线程,而每个线程又可以运行很多协程。线程要负责对协程进行调度,保证每个协程都有机会得到执行。当一个协程睡眠时,它要将线程的运行权让给其它的协程来运行,而不能持续霸占这个线程。同一个线程内部最多只会有一个协程正在运行。

线程的调度是由操作系统负责的,调度算法运行在内核态,而协程的调用是由 Go 语言的运行时负责的,调度算法运行在用户态。

协程可以简化为三个状态,运行态、就绪态和休眠态。同一个线程中最多只会存在一个处于运行态的协程,就绪态的协程是指那些具备了运行能力但是还没有得到运行机会的协程,它们随时会被调度到运行态,休眠态的协程还不具备运行能力,它们是在等待某些条件的发生,比如 IO 操作的完成、睡眠时间的结束等。

操作系统对线程的调度是抢占式的,也就是说单个线程的死循环不会影响其它线程的执行,每个线程的连续运行受到时间片的限制。

Go 语言运行时对协程的调度并不是抢占式的。如果单个协程通过死循环霸占了线程的执行权,那这个线程就没有机会去运行其它协程了,你可以说这个线程假死了。不过一个进程内部往往有多个线程,假死了一个线程没事,全部假死了才会导致整个进程卡死。

每个线程都会包含多个就绪态的协程形成了一个就绪队列,如果这个线程因为某个别协程死循环导致假死,那这个队列上所有的就绪态协程是不是就没有机会得到运行了呢?Go 语言运行时调度器采用了 work-stealing 算法,当某个线程空闲时,也就是该线程上所有的协程都在休眠(或者一个协程都没有),它就会去其它线程的就绪队列上去偷一些协程来运行。也就是说这些线程会主动找活干,在正常情况下,运行时会尽量平均分配工作任务。

设置线程数

默认情况下,Go 运行时会将线程数会被设置为机器 CPU 逻辑核心数。同时它内置的 runtime 包提供了 GOMAXPROCS(n int) 函数允许我们动态调整线程数,注意这个函数名字是全大写,Go 语言的设计者就是这么任性,该函数会返回修改前的线程数,如果参数 n <=0 ,就不会产生修改效果,等价于读操作。

package mainimport "fmt"
import "runtime"func main() {// 读取默认的线程数fmt.Println(runtime.GOMAXPROCS(0))// 设置线程数为 10runtime.GOMAXPROCS(10)// 读取当前的线程数fmt.Println(runtime.GOMAXPROCS(0))
}

获取当前的协程数量可以使用 runtime 包提供的 NumGoroutine() 方法

package mainimport "fmt"
import "time"
import "runtime"func main() {fmt.Println(runtime.NumGoroutine())for i:=0;i<10;i++ {go func(){for {time.Sleep(time.Second)}}()}fmt.Println(runtime.NumGoroutine())
}

协程的应用

在日常互联网应用中,Go 语言的协程主要应用在HTTP API 应用、消息推送系统、聊天系统等。

在 HTTP API 应用中,每一个 HTTP 请求,服务器都会单独开辟一个协程来处理。在这个请求处理过程中,要进行很多 IO 调用,比如访问数据库、访问缓存、调用外部系统等,协程会休眠,IO 处理完成后协程又会再次被调度运行。待请求的响应回复完毕后,链接断开,这个协程的寿命也就到此结束。

在消息推送系统中,客户端的链接寿命很长,大部分时间这个链接都是空闲状态,客户端会每隔几十秒周期性使用心跳来告知服务器你不要断开我。在服务器端,每一个来自客户端链接的维持都需要单独一个协程。因为消息推送系统维持的链接普遍很闲,单台服务器往往可以轻松撑起百万链接,这些维持链接的协程只有在推送消息或者心跳消息到来时才会变成就绪态被调度运行。

聊天系统也是长链接系统,它内部来往的消息要比消息推送系统频繁很多,限于 CPU 和 网卡的压力,它能撑住的连接数要比推送系统少很多。不过原理是类似的,都是一个链接由一个协程长期维持,连接断开协程也就消亡。

千军万马跑协程goroutine相关推荐

  1. Go学习—千军万马跑协程

    协程和通道是 Go 语言作为并发编程语言最为重要的特色之一,初学者可以完全将协程理解为线程,但是用起来比线程更加简单,占用的资源也更少.通常在一个进程里启动上万个线程就已经不堪重负,但是 Go 语言允 ...

  2. 《快学 Go 语言》第 11 课 —— 千军万马跑协程

    协程和通道是 Go 语言作为并发编程语言最为重要的特色之一,初学者可以完全将协程理解为线程,但是用起来比线程更加简单,占用的资源也更少.通常在一个进程里启动上万个线程就已经不堪重负,但是 Go 语言允 ...

  3. go 怎么等待所有的协程完成_GO语言基础进阶教程:Go语言的协程——Goroutine

    Go语言的协程--Goroutine 进程(Process),线程(Thread),协程(Coroutine,也叫轻量级线程) 进程进程是一个程序在一个数据集中的一次动态执行过程,可以简单理解为&qu ...

  4. golang协程goroutine

    协程goroutine 概念 协程(英语:coroutine)是计算机程序的一类组件,推广了协作式多任务的子例程,允许执行被挂起与被恢复.相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那 ...

  5. 3-Go并发编程与协程Goroutine

    目录 一.并发编程 1 - 并行和并发 2 - 程序.进程.线程 3 - 进程并发 4 - 线程并发 5 - 线程同步 二.协程Coroutine 1 - 协程概念 2 - Go并发 3 - go程创 ...

  6. golang协程goroutine简介

    文章目录 goroutine 与thread比较 M:N模型 调度策略 可运行队列 协作式调度 系统调用 同步调用 异步调用 scheduler的陷阱 goroutine是Go语言中的轻量级线程实现, ...

  7. go 协程(Goroutine)详解

    什么是协程 一种比线程更加轻量级的存在.正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程,协程的切换和创建完全是用户决定的. 进程.线程.协程对比 线程和进程是完全由操作系统的分配的,对操 ...

  8. Golang协程goroutine和管道channel结合案例

    管道的遍历和关闭 协程和管道结合案例 需求 思路分析 写数据管道 读数据管道 读完数据后关闭管道,并往exitChain管道中写入数据 主程序

  9. Golang协程goroutine的调度与状态变迁分析

    前言 Go运行时的调度器其实可以看成OS调度器的某种简化版 本,一个goroutine在其生命周期之中,同样包含了各种状态的变换.弄清了这些状态及状态间切换的原理,对搞清整个Go调度器会非常有帮助. ...

最新文章

  1. php adodb使用,常用的php ADODB使用方法集锦
  2. 秦川团队《科学》刊发研究:新冠感染恒河猴康复后不会再感染
  3. IBM又双叒叕要分拆了,IT基础设施部门将剥离,未来专注云计算和AI
  4. 翻译:打造Edge渲染内核的浏览器
  5. 【Hadoop Summit Tokyo 2016】Hivemall: Apache Hive/Spark/Pig 的可扩展机器学习库
  6. AngularJS 1.3 支持使用 $digest() 循环实现延迟
  7. ldd /usr/bin/mysql_mysql客户端登录时报mysql: relocation error错误
  8. 还驾驭不了4核? 别人已模拟出百万核心上的并行
  9. Array.prototype.slice.call
  10. python上手_Python 上手
  11. 连续液位测量行业调研报告 - 市场现状分析与发展前景预测
  12. JavaScript 简介 1
  13. kafka消息堆积原因解析
  14. 左手用R右手Python系列14——日期与时间处理
  15. html最大化和最小化,电脑上最大化最小化图标变了怎么办
  16. 破解XP 管理员Administrator密码
  17. Stream Editor 流编辑器命令
  18. iOS基础-小Demo--刮开涂层(刮刮乐效果)
  19. 共享充电步入“大三元”时代,三电一兽们吃得饱吗?
  20. 用计算机录音并播放教学设计,八年级信息技术《录制声音》说课稿

热门文章

  1. 转载:东拉西扯:产业链
  2. 详解MySQL事务隔离
  3. python pip安装报错_python pip安装requests时报错,怎么解决
  4. r语言liftchart_R语言强大的绘图功能--附数据和代码
  5. 【写作技巧】绪论写作要点
  6. 看似无聊的python小游戏 我却摸鱼上班玩了一下午!!
  7. c语言学生成绩管理系统课设作业,C语言课程设计——学生成绩管理系统
  8. 最强光源解析,做纺织的你知道D65,CWF,TL84,U30,HOR的区别吗?
  9. 形态学操作之提取水平与垂直直线
  10. 深度探索QT窗口系统——几何篇