4.1 go 语言中的并发特性 #
本节源码位置 https://github.com/golang-minibear2333/golang/blob/master/4.concurrent/4.1-goroutine/
以前我们写并发的程序一般是用多线程来实现,自己维护一个线程池,在恰当的时候创建、销毁、分配资源。
go
在并发方面为我们提供了一个语言级别的支持, goroutine
和 channel
相互配合,这决定了他的先天优势。
goroutine
也就是go协程,概念类似于线程, Go
程序运行时会自动调度和管理,系统能智能地将 goroutine
中的任务合理地分配给 CPU
, 让这些任务尽量并发运作。
这里说并发其实是不严谨的,只不过我们习惯了说并发,延展阅读见 goroutine是并行还是并发?
4.1.1 他和线程对比 #
从使用上讲
- 比线程更轻量级,可以创建十万、百万不用担心资源问题。
- 和
channel
搭配使用,实现高并发,goroutine
之间传输数据更方便。 - 如果访问同一个数据块,要小心数据竞态问题、共享锁还是互斥锁的选择问题、并发操作的数据同步问题(后面会说)
从其实现上讲
- 从资源上讲,线程的栈内存大小一般是固定的一般为
2MB
,虽然这个数值可以设置,但是 太大了浪费,太小了容易不够用, 而goroutine
栈内存是可变的,初始一般为2KB
,随着需求可以扩大达到 1GB。 所以goroutine
十分的轻量级,且能满足不同的需求。 - 从调度上讲,线程的调度由
OS
的内核完成;线程的切换需要 CPU 寄存器 和 内存的数据交换 ,从而切换不同的线程上下文。 其触发方式为CPU时钟
, 而goroutine
的调度则比较轻量级,由自身的调度器完成。 - 协程同线程的关系,有些类似于 线程同进程的关系。
4.1.2 创建与使用 #
创建一个 goroutine
,只需要在函数前加一个 go
关键字就成了。
go 函数名(参数)
看一个 demo
func quickFun(){
fmt.Println("maybe you can's see me!")
}
func main(){
go quickFun() // 创建了一个 goroutine
fmt.Println("hey")
time.Sleep(time.Second)
}
goroutine
和main
主线程同时运行main
运行结束会暴力终止所有协程,所以上面的程序多等待了 1 秒Go
程序从main
包的main()
函数开始,在程序启动时,Go
程序就会为main()
函数创建一个默认的goroutine
。
输出
hey
maybe you can's see me!
对,就是这么简单,如果你的函数只在这里使用,也可以用匿名函数来创建 goroutine
。
func main(){
go func() {
fmt.Println("hello ")
}()
time.Sleep(time.Second) //main运行结束会暴力终止所有协程,所以这里先等待1秒
}
PS: 和线程不同,goroutine
没有唯一的id
,所以我们没办法专门针对某个协程进行操作。
4.1.3 体验并发 #
当执行 goroutine 时候,Go 语言立即返回,接着执行剩余的代码,不会阻塞主线程。
下面我们通过一小段代码来讲解 go 的使用:
//首先我们先实现一个 Add()函数
func Add(a, b int) {
c := a + b
fmt.Println(c)
}
go Add(1, 2) //使用go关键字让函数并发执行
该函数就会在一个新的 goroutine 中并发执行,当该函数执行完毕时,这个新的 goroutine 也就结束了。
不过需要注意的是,如果该函数具有返回值,那么返回值会被丢弃。所以什么时候用 go 还需要酌情考虑。
接着我们通过一个案例来体验一下 Go 的并发到底是怎么样的。新建源文件 goroutine2.go,输入以下代码:
package main
import "fmt"
func Add(a, b int) {
c := a + b
fmt.Println(c)
}
func main() {
for i := 0; i < 10; i++ {
go Add(i, i)
}
}
执行 goroutine.go 文件会发现屏幕上什么都没有,但程序并不会报错,这是什么原因呢?
原来当主程序执行到 for 循环时启动了 10 个 goroutine,然后主程序就退出了,而启动的 10 个 goroutine 还没来得及执行 Add() 函数,所以程序不会有任何输出。也就是说主 goroutine 并不会等待其他 goroutine 执行结束。
并发等待的问题我将在下一节进行介绍。
4.1.4 小结 #
学 go 语言必学并发,通过本节我们知道了 goroutine
是 Go 语言并行设计的核心。十几个 goroutine
可能在底层就是几个线程。 实际上是 Go 在 runtime
系统调用等多方面对 goroutine
调度进行了封装和处理。
协程是非常容易创建的,而且他非常轻量只占用2k,其他语言最小大多都是 MB
,协程的使用还要配合数据传输,生产者消费者模型,关于协程的调度,我们后续再说。
另外并发 bug 的定位和解决是老大难的问题了,平时就要注意的良好的代码风格和编程习惯。
拓展知识(栈内存):
关于 goroutine stack size(栈内存大小)
官方的文档 中所述,1.2
之前最小是4kb,在1.2
变成8kb,并且可以使用
SetMaxStack 设置栈最大大小。
在
runtime/debug 包能控制最大的单个 goroutine
的堆栈的大小。在 64 位系统上默认为 1GB,在 32 位系统上默认为 250MB。
因为每个goroutine需要能够运行,所以它们都有自己的栈。假如每个goroutine分配固定栈大小并且不能增长,太小则会导致溢出,太大又会浪费空间,无法存在许多的goroutine。
所以在 1.3版本中,改为了 Contiguous stack( 连续栈 ),为了解决这个问题,goroutine可以初始时只给栈分配很小的空间(8KB),然后随着使用过程中的需要自动地增长。这就是为什么Go可以开千千万万个goroutine而不会耗尽内存。
1.4 版本 goroutine 堆栈从 8Kb 减少到 2Kb
拓展阅读