第九章 goroutine
接下来学习并发编程, 并发编程是go语言最有特色的地方, go对并发编程是原生支持.
goroutine是go中最近本的执行单元
每一个go程序至少有一个goroutine, 那就是主goroutine. 当程序启动时, 他会自动创建. 也就是main方法
main方法也是一个goroutine
一. 如何定义一个协程.
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i<1000; i++ {
go func(i int) {
for {
fmt.Printf("goroutine: %d n", i)
}
}(i)
}
time.Sleep(time.Second)
}
- 定义协程使用go 关键字.
二. 对goroutine的理解
goroutine和Coroutine比较相似, Coroutine是协程. 其他语言都有这个叫法, 但不是所有语言都支持.
- 协程和线程的区别
- 线程(Thread):有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。
线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程的切换一般也由操作系统调度。
-
-
协程(Coroutine): 是一种轻量级的"线程", 作用是处理并发任务, 这一点和线程差不多. 协程为什么是轻量级的线程呢?
- 非抢占式多任务处理, 由协程主动交出控制权. 对比线程, 线程随时都有可能被cpu切换, 线程是抢占式任务处理. 我们是没有控制权的. 任务执行一半, 操作系统有可能就切换到另一个线程去了. 然后还有可能在切换回来. 那么这样切换的时候, 就要考虑保存切换前的状态. 协程不同, 协程是非抢占式的, 什么时候交出控制权, 由协程自己说了算. 因为是非抢占式, 所以不用存储那么多状态, 节省了很多资源
- 编译器/解释权/虚拟机层面的多任务. 线程是操作系统层面的多任务. 而协程是虚拟机,编译器, 解释器层面的多任务. 在go语言中, 协程可以看作是编译器级别的多任务. 编译器会把一个go func解释为一个协程. 具体在执行上, go语言后面会有一个调度器. (操作系统有一个调度器, go语言还有自己的调度器)
- 多个协程可以在一个或多个线程上运行. 这是由go调度器决定的
-
协程(Coroutine): 是一种轻量级的"线程", 作用是处理并发任务, 这一点和线程差不多. 协程为什么是轻量级的线程呢?
1. 非抢占式多任务处理
package main
import (
"fmt"
"time"
)
func main() {
var a [10]int
for i := 0; i<10; i++ {
go func(i int) {
for {
a[i] ++
}
}(i)
}
time.Sleep(time.Second)
fmt.Println(a)
}
猜一下, 这段代码的运行结果. 结合非抢占式多任务处理
结果是: 这段代码是一个死循环. 当第一次进入到循环体以后. 由于goroutine是非抢占式, 所以第一次循环一直持有, 没有主动释放. 所以, 这段代码的结果是死循环
2. 手动交出控制权
runtime.Gosched()
这样就可以手动交出控制权, 让其他协程运行
3. race condition 数据访问冲突
如果我们在协程中没有传变量i会怎么样呢?
没错, 报错了. 为什么报错了呢? 我们通过race 来看一下
go run -race goroutine.go
可以看到报错的原因是, 同一块空间, 在第七个协程读, 在主协程写. 这样就是有问题的了.
接下来分析一下这段代码为什么报错?
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
var a [10]int
for i := 0; i < 10; i++ {
go func() {
for {
a[i] ++
runtime.Gosched()
}
}()
}
time.Sleep(time.Second)
fmt.Println(a)
}
程序启动的时候都做了哪些事?
首先. 开了10个协程. i从1遍历到10, 发现10 < 10 , for循环退出了. 但是, 由于协程里面的i是直接饮用的外部的i. 当for循环完成以后, i的值变成10了. 协程里对a[10]进行++, 那自然就会报异常了.
所以, 为了安全起见, 我们把每一次开协程的时候, 把i带过去.
修改后的
func main() {
var a [10]int
for i := 0; i < 10; i++ {
go func(i int) {
for {
a[i] ++
runtime.Gosched()
}
}(i)
}
time.Sleep(time.Second)
fmt.Println(a)
}
这时候我们在-race一下, 查看是否还有数据访问冲突
依然有数据访问冲突. 主goroutine在读, 第7个协程在写. 所以这样是有问题的. 这个问题可以通过chan来解决.
4. 子程序是协程的一个特例
我们知道每一个函数都是一个子程序, 子程序是协程的一个speical case, 那怎样才算是一个special case呢?
普通函数和协程的区别
1) 普通函数的调用: 首先main方法启动, main方法里调了另一个doWork方法. 当doWork方法都执行完了以后, 在继续回到main方法里, 一次往下执行. 所以普通的函数是单线程.
2) 协程的调用: 协程也是main和doWork, 但是main和doWork之间不是单向的箭头. 中间有一个双向的通道.
main和dowork之间可以双向的流通. 控制权也可以双向的流通.就像两个线程, 各做各的事情, 中间还可以通信, 控制权可以相互交换.
那么main和dowork运行在哪里呢?
可能是一个线程, 也可能是多个线程. 这个事情不需要程序员管了, 调度器可能开一个线程,也可能开两个线程进行执行.
5. go语言的协程
1) 首先有一个go语言的进程, 他下面会有一个调度器, 调度器的作用就是调度协程
2) 调度器会分配, 一个协程在一个线程里运行, 也可能是两个协程在一个线程里运行, 也可能是多个协程在一个线程里运行. 这是调度器做的事, 程序员不用管.
6. 协程的定义
1) 在函数前加go, 就可以交给调度器运行
2) 不需要再定义时区分是否是异步函数. 这个是相对于python来说的
3) 调度器在合适的点进行切换. 由调度器操作执行, 一般不需要我们来操作
4)使用-race来检测数据访问冲突. 这个在上面已经讲过了.
7. goroutine可能切换的点
调度器在哪些个点有可能切换协程呢?
1. I/O, select : I/O和select可能会切换. 之前fmt.Println("")为什么会切换呢? 因为他是一个I/O
2) channel
3) 等待锁
4) 函数调用(有时)
5) runtime.Gosched() 手动提供切换的点
总结: 以上只是一个参考, 不能保证遇到这些地方一定切换, 也不能保证其他地方就不切换.
8. 观察启动1000个协程, 我们的系统分配了多少个线程.
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 1000; i++ {
go func(i int) {
for {
fmt.Printf("hello goroutine: %d n", i)
}
}(i)
}
time.Sleep(time.Minute)
}
上面这段程序, 看重点
1) 开了1000个协程
2) 主线程等待1分钟
我们top一下, 看看效果
红色圈出的是go运行的程序
cpu的占用率是186.6%, 12/3 表示开了多少个线程. 我们发现开了12个线程, 但是最终运行的线程数最多是4个. 原因是我的cpu是4核的.
- 比较R语言机器学习算法的性能
- 【专业文章】六种常见的HTML5写法误用(二)
- CSS3动画,为你带来极致的视觉体验!
- 【高级编程】linux进程间通信总结
- JS 吸顶导航,告别“回到顶部”
- 用AlphaGo来做股票交易会怎样?机器学习预测股票靠谱么?
- 开发 | 深度神经网络可视化工具集锦
- CSS3三维变形,其实很简单!
- 使用 pandas处理股票数据并作分析
- 用R语言做时间序列分析(附数据集和源码)
- 【android开发】Android GUI系统学习1:Gralloc
- 【kaggle实战】从KNN,LR,SVM,RF到深度学习
- 开发 | 训练一个AI给颜值打分,公平公正!
- 【android开发】Android HAL模块实现
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法