Golang 经过编译器运转时(Runtime),从言语上原生支撑了并发编程。

并发与并行

学习 go 并发编程之前,咱们需求弄清并发、并行的概念。

因为 CPU 同一时间只能履行一个进程/线程,在下文的概念中,咱们把进程/线程统称为使命。不同的场景下,使命所指的或许是进程,也或许是线程。

并发(Concurrency)

并发是指核算机在同一时间段内履行多个使命。

并发的概念比较宽泛,它单纯是指核算机能够一起履行多个使命。比方咱们当前是一个单核的 CPU,可是咱们有5个使命,核算机会经过某种算法将 CPU 资源合理的分配给多个使命,从用户角度来看的话便是多个使命在一起履行。前面说的的算法比方“时间片轮转”。

Golang 并发编程

并行(Parallelism)

并行是指在同一时间履行多个使命。 当咱们有多个中心的 CPU 的时分,咱们一起履行两个使命,就不需求经过“时间片轮转”的方法让多个使命替换履行了,一个 CPU 担任一个使命,同一时间,多个使命一起履行,这便是并行。

Golang 并发编程

并发+并行

上面的并行图中所展现的使命履行机制,是一种理想化的状况,即履行使命的数量等于 CPU 的中心数量。但实际的场景中,使命数是远大于 CPU 的中心数量的。比方我的电脑是8核的,可是我开机就要发动几十个使命,这个时分就会出现并发和并行都存在的状况。

并行和并发的区别

并发和并行的底子区别是使命是否一起履行。
并发不是并行。并行是让不同的代码片段一起在不同的物理处理器上履行。并行的关键是一起做许多工作,而并发是指一起办理许多工作,这些工作或许只做了一半就被暂停去做别的工作。
在许多状况下,并发的效果比并行好,因为操作系统和硬件的总资源一般很少,但能支撑系统一起做许多工作。这种“使用较少的资源做更多的工作”的哲学,也是辅导Go言语规划的哲学。

goroutine

在了解 goroutine 之前咱们先了解一下什么是协程(coroutine)。

协程(Coroutine)

这儿咱们不再多赘述进程、线程的联系,咱们来看下协程。协程是其他编程言语中的一种叫法,但并不是一切编程言语都支撑 coroutine。 所以协程是什么?

  1. 轻量级的“线程”:效果和线程差不多,都是并发履行一些使命的。
  2. 非抢占式多使命处理,即由协程自动交出控制权。这儿需求了解一下抢占式非抢占式的区别:
    • 抢占式:以线程为例,线程在任何时分都或许被操作系统进行切换,所以线程就叫做抢占式使命处理,即线程没有控制权,使命即便做到一半,哪怕没有做完,也会被操作系统给抢占了,然后切换到其他使命去。
    • 非抢占式:非抢占式的代表便是协程了,协程在使命做到一半的时分能够自动的交出使命的控制权,控制权是由协程内部决议,也正是因为这一特性,协程才是轻量级的。需求留意的是,当一个协程不自动交出控制权的时分,或许会形成死锁,也便是说控制权会一向在这个协程内部,程序将长期等候,无法跳出。
  3. 编译器/解释器/虚拟机层面的多使命,非操作系统层面的,操作系统层面的多使命就只有进程/线程。
  4. 多个协程或许在一个或多个线程上运转,大多数状况下由调度器决议。
  5. 子程序(函数调用,比方 func a() {})是协程的一个特例。

这儿需求解释一下第5点,为什么子程序是协程的一个特例的。
咱们来看一下一般函数和协程的比照: 一般函数: 在一个线程内,有一个 mian 函数,main 函数调用函数 work, 然后 work 开端履行,work 履行完毕后会把控制权交给 main 函数,然后 main 函数会履行后边的函数等。

Golang 并发编程

协程: 协程中 main 函数和 work 函数之间是有个双向的通道(下图中是双箭头),彼此经过通道来进行通讯,且两者的控制权也能够双向的交流。那么协程运转在哪里呢?或许是运转在同一个线程,也能够分别运转在不同的线程。协程详细是怎样被分配的,一般作为应用层的使用者来说,咱们是不用关心的,这些彻底是由调度器来完成操作的。

Golang 并发编程

关于协程第2点控制权的部分,后边咱们讲到 goroutine 的时分学习一下怎样“迫使”协程自动交出控制权的方法,这儿暂时就先不详细说明晰。

go 言语的协程(goroutine)

goroutine 其实是一种协程,或者说和协程比较像。

在上文中咱们了解了通用编程言语中的协程概念后,终于轮到今天的主角 goroutine 了。咱们先来看一下 goroutine 模型。 看图比较容易了解,首先是 go 程序发动一个进程,一起发动一个调度器,在这个调度器之上会分配 goroutine 的调度,也便是上面说到的,多个 goroutine 或许分配在一个线程中,也或许被分配到不同的线程中。

Golang 并发编程

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)
}
Golang 并发编程

履行修改后的代码发现,IDE 显示程序一向处于运转状况,咱们再来经过 top 指令查看一下电脑 CPU 使用状况:

Golang 并发编程

因为我电脑的 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 来处理,这篇文章里就不过多做介绍了。

以上。