在通用型语言中 原生支持并发编程的并不常见
下面我们先来看一个例子
package main
import (
"fmt"
"time"
)
func main(){
for i := 0; i < 1000; i ++ { //一千个人同时运行
go func(it int){ //go关键字支持并发运行
for {
fmt.Println("hello wold!", it) //运行的目的是打印这句话
}
}(i) //将i传入匿名函数 直接在匿名函数中使用i会造成程序崩溃 下面会详细讲到
}
time.Sleep(time.Millisecond) //这里给他一个限制 只允许运行一毫秒
}
go语言的并发关键字就是go
后面跟需要并发的函数名称 但一般情况下使用匿名函数
需要注意的是 当main函数退出时 所有并发都会被杀掉 以这个例子来说time.sleep就是让main函数延迟1毫秒退出
例子中是让1000个人同时打印 对一般编程语言而言(不算第三方) 相同环境下已经比较吃力 而go语言还可以增加更多
这主要得益于go语言的关键字go 并发处理开的是协程(Coroutine)而不是线程
协程是轻量级线程 粗略来说 协程和线程相同 都是处理并发开多个线程的 但是协程是轻量级的
协程为什么是轻量级的 下面列出几个关键点
非抢占式多任务处理,由协程主动交出控制权
线程在任何时候都可以被系统切换 所有线程又称之为抢占式多任务处理 哪怕程序执行到一半 都可以被系统切换
协程是主动交出控制权 这是协程轻量级的关键
编译器/解释器/虚拟机层面的多任务
协程可以看作是编译器层面的多任务而不是操作系统的多任务 操作系统只有线程没有协程
具体是go语言在执行goroutine时 编译器会有一个调度器为他调度协程 操作系统有调度器 go语言有自己的调度器
多个协程可以在同一个或者多个进程上运行
下面我们来看一个协程不主动交出控制权的代码
func main(){
var a[15]int
for i := 0; i <15; i ++ {
go func(i int){
for {
a[i]++ //死循环 无法交出控制权
}
}(i)
}
time.Sleep(time.Millisecond)
fmt.Println(a)
}
不交出控制权的后果就是其他线程都在等待它交出控制权(交出控制权不代表线程结束)所以这是一个死循环
那我们有什么办法让他主动交出控制权呢 除了调用 io.函数之外(fmt.print...之类的函数就是调用io.)
那就是在循环结尾加 runtime.Gosched()
func main(){
var a[15]int
for i := 0; i <15; i ++ {
go func(i int){
for {
a[i]++ //死循环 无法交出控制权
runtime.Gosched()
}
}(i)
}
time.Sleep(time.Millisecond)
fmt.Println(a)
}
runtime.Gosched()相当于从火车上离开 让给别的乘客(线程)但是自身可以继续乘坐火车 只要自身需要
大部分情况下都不会用到runtime.Gosched()
从上面代码我们可以发现 循环i值是作为参数进入匿名函数的 为什么不直接使用i值呢
因为当我们匿名函数直接使用i值时会报错
func main(){
var a[15]int
for i := 0; i <15; i ++ {
go func(){
for {
a[i]++
runtime.Gosched()
}
}()
}
time.Sleep(time.Millisecond)
fmt.Println(a)
}
我们使用上面命令行代码可以打印出错误报告
当运行for循环最后一次时 i还有一次++ 数组a[i]就会越界 所以报错
我们继续引入一个新的概念
子程序是协程的一个特例 每一次调用函数时就是开协程
普通函数时如何运行的呢
普通函数
-线程----------------------------------------
| |
| main |
| \|/ |
| dowork |
| |
| |
-----------------------------------------------
在一个线程内 main调用子函数 子函数运行结束后交出控制权 这里采用的是main单项传输到dowork
协程是如何运作的呢
普通函数
-线程(可能多个)------------------------
| |
| main |
| /|\、 |
| \|/ |
| dowork |
| |
-----------------------------------------------
协程是一组双向通道 控制权可以互通 双方可以不在一个线程 只有这样才能保证并发的数量
至于几个协程共用一个线程是如何计算的 不归我们管 这是调度器自己控制的
goroutine可能的切换点
.I/O,select
.channel
.等待锁
.函数调用(有时)
.runtime.Gosched()
只是参考,不能保证切换,不能保证在其他地方不切换
总结goroutine
.任何函数只要加上go 就能送给调度器运行
.不需要在定义时 区分是否异步
.调度器在合适的点进行切换 人为不能完全控制
.使用-race检测数据访问冲突
最后一句话总结就是go语言的线程是非抢占式的