Golang 经过编译器运转时(Runtime),从言语上原生支撑了并发编程。
并发与并行
学习 go 并发编程之前,咱们需求弄清并发、并行的概念。
因为 CPU 同一时间只能履行一个进程/线程,在下文的概念中,咱们把进程/线程统称为使命。不同的场景下,使命所指的或许是进程,也或许是线程。
并发(Concurrency)
并发是指核算机在同一时间段内履行多个使命。
并发的概念比较宽泛,它单纯是指核算机能够一起履行多个使命。比方咱们当前是一个单核的 CPU,可是咱们有5个使命,核算机会经过某种算法将 CPU 资源合理的分配给多个使命,从用户角度来看的话便是多个使命在一起履行。前面说的的算法比方“时间片轮转”。
并行(Parallelism)
并行是指在同一时间履行多个使命。 当咱们有多个中心的 CPU 的时分,咱们一起履行两个使命,就不需求经过“时间片轮转”的方法让多个使命替换履行了,一个 CPU 担任一个使命,同一时间,多个使命一起履行,这便是并行。
并发+并行
上面的并行图中所展现的使命履行机制,是一种理想化的状况,即履行使命的数量等于 CPU 的中心数量。但实际的场景中,使命数是远大于 CPU 的中心数量的。比方我的电脑是8核的,可是我开机就要发动几十个使命,这个时分就会出现并发和并行都存在的状况。
并行和并发的区别
并发和并行的底子区别是使命是否一起履行。
并发不是并行。并行是让不同的代码片段一起在不同的物理处理器上履行。并行的关键是一起做许多工作,而并发是指一起办理许多工作,这些工作或许只做了一半就被暂停去做别的工作。
在许多状况下,并发的效果比并行好,因为操作系统和硬件的总资源一般很少,但能支撑系统一起做许多工作。这种“使用较少的资源做更多的工作”的哲学,也是辅导Go言语规划的哲学。
goroutine
在了解 goroutine 之前咱们先了解一下什么是协程(coroutine)。
协程(Coroutine)
这儿咱们不再多赘述进程、线程的联系,咱们来看下协程。协程是其他编程言语中的一种叫法,但并不是一切编程言语都支撑 coroutine。 所以协程是什么?
- 轻量级的“线程”:效果和线程差不多,都是并发履行一些使命的。
-
非抢占式多使命处理,即由协程自动交出控制权。这儿需求了解一下抢占式和非抢占式的区别:
- 抢占式:以线程为例,线程在任何时分都或许被操作系统进行切换,所以线程就叫做抢占式使命处理,即线程没有控制权,使命即便做到一半,哪怕没有做完,也会被操作系统给抢占了,然后切换到其他使命去。
- 非抢占式:非抢占式的代表便是协程了,协程在使命做到一半的时分能够自动的交出使命的控制权,控制权是由协程内部决议,也正是因为这一特性,协程才是轻量级的。需求留意的是,当一个协程不自动交出控制权的时分,或许会形成死锁,也便是说控制权会一向在这个协程内部,程序将长期等候,无法跳出。
- 编译器/解释器/虚拟机层面的多使命,非操作系统层面的,操作系统层面的多使命就只有进程/线程。
- 多个协程或许在一个或多个线程上运转,大多数状况下由调度器决议。
- 子程序(函数调用,比方
func a() {}
)是协程的一个特例。
这儿需求解释一下第5点,为什么子程序是协程的一个特例的。
咱们来看一下一般函数和协程的比照:
一般函数:
在一个线程内,有一个 mian 函数,main 函数调用函数 work, 然后 work 开端履行,work 履行完毕后会把控制权交给 main 函数,然后 main 函数会履行后边的函数等。
协程: 协程中 main 函数和 work 函数之间是有个双向的通道(下图中是双箭头),彼此经过通道来进行通讯,且两者的控制权也能够双向的交流。那么协程运转在哪里呢?或许是运转在同一个线程,也能够分别运转在不同的线程。协程详细是怎样被分配的,一般作为应用层的使用者来说,咱们是不用关心的,这些彻底是由调度器来完成操作的。
关于协程第2点控制权的部分,后边咱们讲到 goroutine 的时分学习一下怎样“迫使”协程自动交出控制权的方法,这儿暂时就先不详细说明晰。
go 言语的协程(goroutine)
goroutine 其实是一种协程,或者说和协程比较像。
在上文中咱们了解了通用编程言语中的协程概念后,终于轮到今天的主角 goroutine 了。咱们先来看一下 goroutine 模型。
看图比较容易了解,首先是 go 程序发动一个进程,一起发动一个调度器,在这个调度器之上会分配 goroutine 的调度,也便是上面说到的,多个 goroutine 或许分配在一个线程中,也或许被分配到不同的线程中。
goroutine 的界说
- 一般给函数加上 go 关键字,就能交给调度器调用:
go 函数名(参数列表) // 具名函数方法
go func(参数列表) { // 匿名函数方法
函数体
}(调用参数列表)
- 界说时无需区别函数是否异步,python 中协程的界说需求用到
async
关键字 - 调度器会在合适的机遇切换 goroutine,即便 goroutine 是非抢占式的,可是操作权并不彻底在 goroutine,这也是 goroutine 和传统协程的一点区别
使用 goroutine
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Print("i:", i, ", ")
}(i)
}
}
Go 程序一般从 main 包的 main 函数开端,在程序发动的时分,Go 程序就会为 main 函数创建一个默认的 goroutine,需求留意的是使用 go 关键字创建 goroutine时,被调用的函数的返回值会被疏忽。 履行上面的代码输出成果是:
/private/var/folders/rh/6jh584kn2jb7fbp2ymcjw9800000gn/T/GoLand/___go_build_go_leaning_go_routine
Process finished with the exit code 0
很古怪,明明fmt.Print
了,但第2行什么都没有打印,接着程序就直接退出了。原因是因为程序中 main 函数 和 其他 goroutine 是并发履行的,for 循环履行完之后就直接跳出循环,main 就退出了,代码中的 Print goruntine 还来不及打印就被程序干掉了。
怎样看到成果呢?main 程序慢点退出就能够了,稍微加点料:
func main() {
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Print("i:", i, ", ")
}(i)
}
time.Sleep(time.Millisecond) // 延迟 main 函数退出
}
// result:
// i:0, i:2, i:5, i:4, i:6, i:7, i:8, i:9, i:3, i:1,
// Process finished with the exit code 0
再稍微改动一下代码,让 goroutine 无法自动交出控制权:
func main() {
var arr [10]int
for i := 0; i < 10; i++ {
go func(i int) {
for {
arr[i]++
}
}(i)
}
time.Sleep(time.Minute) // 休眠1分钟
fmt.Print("arr: ", arr)
}
履行修改后的代码发现,IDE 显示程序一向处于运转状况,咱们再来经过 top 指令查看一下电脑 CPU 使用状况:
因为我电脑的 CPU 是8核的,假如 CPU 打满的话是 占用率应该是800%,上图能看到的是 goroutine 已经占用了716.%。休眠完毕后能够看一下详细输出:
arr: [10582140406 10463247362 10747009051 10642989545 10505259520 10456203629 10500957117 10661942229 10440913209 10357335909]
goroutine 交出控制权
上面说协程(coroutine)的时分讲到,非抢占式使命能够自动交出控制权,咱们看下 goroutine 是怎样交出控制权的方法:
1)I/O 操作交出控制权:
其实 I/O 操作咱们上面已经看到过了,便是 fmt.Print()
等…
func main() {
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Print("i:", i, ", ")
}(i)
}
time.Sleep(time.Millisecond)
}
2)runtime.Gosched() :
func main() {
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Print("i:", i, ", ")
runtime.Gosched()
}(i)
}
time.Sleep(time.Millisecond)
}
3)select 操作,大致结构如下:
select {
case <- chan1: // 假如 chan1 成功读到数据就履行该 case 句子处理
case chan2 <- 1 // 假如成功向 chan2 写入数据,就履行该 case 句子
default: // 假如以上都没成功就履行该句子
}
需求留意的是,每个 case 句子都必须是面向 channel
操作的。
4)channel:
func getData(values chan int) {
value := rand.Intn(20)
values <- value
}
func main() {
values := make(chan int)
go getData(values)
value := <-values
fmt.Println("Channel value: ", value)
}
5)等候锁,即传统模式的锁同步机制:
能够经过 sync.Mutex
完成,这儿就不多赘述了。
6)函数调用(有时会):
我个人了解是,如 time.sleep()
的 Sleep 函数,func Sleep(){}
的官方 API 解释是:
Sleep pauses the current goroutine for at least the duration d 即 sleep 当前的 goroutine d duration 时间。
7)其他…
goroutine 闭包陷阱
仍是先来看一段代码:
func main() {
var arr [10]int
for i := 0; i < 10; i++ {
go func() {
for {
arr[i]++ // look!
runtime.Gosched()
}
}()
}
time.Sleep(time.Millisecond)
fmt.Print("arr:", arr)
}
这段代码只是把参数列表和调用参数列表给移除了,变量 i
直接使用了 for 循环界说的 i
,这时分再运转代码,控制台就会报错,程序 panic 了:
panic: runtime error: index out of range [10] with length 10
假如不明白这个问题的话,go 言语供给了咱们一个指令去排查问题,
-race
指令便是咱们去检测数据的冲突:
go run -race routine1.go
使用该指令运转程序之后会打印许多日志,咱们挑要点的看:
==================
WARNING: DATA RACE
Read at 0x00c00013c018 by goroutine 7:
...
Previous write at 0x00c00013c018 by main goroutine:
...
WARNING: DATA RACE
Read at 0x00c00013e010 by goroutine 7:
...
Previous write at 0x00c00013e010 by goroutine 8:
要点看一下 WARNING: DATA RACE
,这儿的 RACE
指的便是竞态(race condition)。
再看下上面的日志,Read at 0x00c00013c018 by goroutine 7
这是个经过 goroutine 7
进行的读操作,Previous write at 0x00c00013c018 by main goroutine
这是个经过 main goroutine
进行的写操作。这两个操作都指向了同一个内存地址 0x00c00013c018
,这个内存地址代表了什么呢?答案是代表了变量i
。
形成上面竞态的底子原因便是闭包陷阱,即 for 循环履行完之后 i
被设置为10,终究每个 goroutine 操作的都是 arr[10]
,所以就会出错。
处理方法的话就能够经过参数列表和调用参数列表每次复制一个新 i
的值给 goroutine 就能够了。
咱们加上参数列表和调用参数列表再运转一下程序,成果:
arr:[582 592 628 647 489 568 490 618 529 400]
再经过 -race
指令跑一下:
==================
WARNING: DATA RACE
Read at 0x00c00013e000 by main goroutine:
runtime.racereadrange()
<autogenerated>:1 +0x1b
Previous write at 0x00c00013e000 by goroutine 7:
main.main.func1()
/Users/li2/Code/go_learning/go_routine/routine1.go:14 +0x64
...
arr:[2695 1962 1651 1580 1519 1604 1275 1477 1484 1506]Found 1 data race(s)
exit status 66
古怪了,仍是有 data race warning,这儿过错有两个,一个是 main goroutine
的读,一个是代码中第 14
行的写,这两行代码分别是:
// main goroutine
fmt.Print("arr:", arr)
// 14 行的
arr[i]++
也便是说程序一边在 fmt.Print(arr)
,又一边在并发履行 arr[i]++
,这个问题要怎样处理呢?
答案是经过 channel
来处理,这篇文章里就不过多做介绍了。
以上。