协程和通道是 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

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

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

-------------
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")
}---------
run in main goroutine
run in child goroutine
run in grand child goroutine
run in grand grand child goroutine
panic: wtfgoroutine 34 [running]:
main.main.func1.1.1()/Users/qianwp/go/src/github.com/pyloque/practice/main.go:14 +0x79
created by main.main.func1.1/Users/qianwp/go/src/github.com/pyloque/practice/main.go:12 +0x75
exit status 2

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

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

启动百万协程

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++}
}

协程死循环

前面我们通过 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")}
}

协程的本质

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

图片

线程的调度是由操作系统负责的,调度算法运行在内核态,而协程的调用是由 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))
}--------
4
10
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())
}------
1
11

协程的应用

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

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

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

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

在后面的高级内容部分,我将会教读者使用协程来实现上面这三个系统。下一章节我们开讲通道,因为通道的使用比较复杂,知识点较多,所以需要单独一节来讲。

阅读《快学 Go 语言》更多章节,长按图片识别二维码关注公众号「码洞」

《快学 Go 语言》第 11 课 —— 千军万马跑协程相关推荐

  1. 千军万马跑协程goroutine

    参考:<快学 Go 语言>第 11 课 -- 千军万马跑协程 - 云+社区 - 腾讯云 协程和通道是 Go 语言作为并发编程语言最为重要的特色之一,初学者可以完全将协程理解为线程,但是用起 ...

  2. 快学 Go 语言 第 3 课 —— 分支与循环

    程序 = 数据结构 + 算法 上面这个等式每一个初学编程的同学都从老师那里听说过.它并不是什么严格的数据公式,它只是对一般程序的简单认知.数据结构是内存数据关系的静态表示,算法是数据结构从一个状态变化 ...

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

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

  4. 《快学 Go 语言》第 5 课 —— 神奇的切片

    切片无疑是 Go 语言中最重要的数据结构,也是最有趣的数据结构,它的英文词汇叫 slice.所有的 Go 语言开发者都津津乐道地谈论切片的内部机制,它也是 Go 语言技能面试中面试官最爱问的知识点之一 ...

  5. go int 转切片_「快学 Go 语言」第 4 课——低调的数组

    数组就是一篇连续的内存,几乎所有的计算机语言都有数组,只不过 Go 语言里面的数组其实并不常用,这是因为数组是定长的静态的,一旦定义好长度就无法更改,而且不同长度的数组属于不同的类型,之间不能相互转换 ...

  6. 《快学 Go 语言》第 7 课 —— 冰糖葫芦串

    字符串通常有两种设计,一种是「字符」串,一种是「字节」串.「字符」串中的每个字都是定长的,而「字节」串中每个字是不定长的.Go 语言里的字符串是「字节」串,英文字符占用 1 个字节,非英文字符占多个字 ...

  7. 《快学 Go 语言》第 7 课 —— 诱人的烤串

    字符串通常有两种设计,一种是「字符」串,一种是「字节」串.「字符」串中的每个字都是定长的,而「字节」串中每个字是不定长的.Go 语言里的字符串是「字节」串,英文字符占用 1 个字节,非英文字符占多个字 ...

  8. 《Go 语言程序设计》读书笔记 (五) 协程与通道

    Goroutines 在Go语言中,每一个并发的执行单元叫作goroutine.设想一个程序中有两个函数,假设两个函数没有相互之间的调用关系.一个线性的程序会先调用其中的一个函数,然后再调用另一个.如 ...

  9. 11 单线程+多任务异步协程 爬虫

    # from lxml import etree import asyncio import aiohttp import time def callback(task): # 回调函数page = ...

最新文章

  1. 【BZOJ4259】残缺的字符串
  2. quick sort
  3. 警惕使用WebClient.DownloadFile(string uri,string filePath)方法
  4. linux7补丁安全,CentOS自动打重要安全补丁
  5. python sanic_如何使用Python和Sanic使代码快速异步
  6. 三个点在同一个半圆的概率_圆形水池中的四只小鸭子出现在同一个半圆中的概率是?...
  7. html实验原理及目的,网页设计实验报告_图文
  8. cad查看_CAD干货:手把手教你如何在手机上查看CAD图纸,赶紧了解一波~
  9. 颜晖c语言设计答案,c语言程序设计 (何钦铭 颜晖 著)课后习题答案
  10. 【snipaste下载和快捷键的修改】
  11. 2021年百度账号批量取消绑定手机号教程
  12. SDN控制器Floodlight源码学习(五)--控制器和交换机交互(3)
  13. 小虎电商浏览器:拼多多怎么看单品实时数据
  14. ArcGIS中根据DEM提取等高线和高程点(附练习数据)
  15. 软件测试大学生求职信英语版,英语专业大学生求职信范文
  16. arcgis中python坡度计算_ArcGIS坡度计算
  17. 【sfu】sdp和扩展的修改和对比
  18. 遗传算法(GA)附Matlab代码(copy能用)寻优算法
  19. 和菜鸟一起学linux总线驱动之初识USB设备描述符
  20. java计算机毕业设计医疗健康管理平台会员管理子系统源码+数据库+系统+lw文档+部署

热门文章

  1. Python学习笔记--调试器debugger
  2. java打怪游戏_HTML5存储(带一个粗糙的打怪小游戏案例)
  3. Web3中文|随着世界杯结束,web3体育可能达到800亿美元
  4. 【2023B题】人工智能对大学生学习影响的评价(思路、代码)
  5. 纯Python read_counts 转FPKM v2
  6. 为什么说FPKM和RPKM都错了?
  7. 选择程序员还是公务员?不要被误导了
  8. 傅里叶变换的高通滤波
  9. Dynamcis CRM XrmToolBox工具之FetchXml Tester
  10. Android 混合开发优缺点