Golang并发(四)
转载自: Golang 基础之并发goroutine(补充)前面 《Golang 基础之并发知识 (二)》 章节已经和大家简单介绍 - 掘金
goroutine是go中最基本的组织单位之一。
事实上,每个go程序至少有一个: main goroutine,当程序开始的时候,会自动创建并启动。
在几乎所有的go程序中,你都可能会发现自己迟早加入到一个goroutine中。
goroutine 对Go来说是独一无二的(尽管其他一些语言有类似的并发原语)。它们不是操作系统线程,它们不完全是绿色的线程(由语言运行时管理的线程),它们是更高级别的抽象,被称为协程(coroutines)。协程是非抢占的并发子程序,也就是说,它们不能被中断。
Go的独特之处在于goroutine与Go的运行时深度整合。Goroutine没有定义自己的暂停或再入点; Go的运行时观察着goroutine的行为,并在阻塞时自动挂起它们,然后在它们变畅通时恢复它们。在某种程度上,这使得它们可以抢占,但只是在goroutine被阻止的地方。它是运行时和goroutine逻辑之间的一种优雅合作关系。 因此,goroutine可以被认为是一种特殊的协程。
协程,因此可以被认为是goroutine的隐式并发构造,但并发并非协程自带的属性:某些东西必须能够同时托管几个协程,并给每个协程执行的机会,否则它们无法实现并发。当然,有可能有几个协程按顺序执行,但看起来就像并行一样,在Go中这样的情况比较常见。
Go的宿主机制实现了所谓的M:N调度器,这意味着它将M个绿色线程映射到N个系统线程。 goroutine随后被安排在绿色线程上。 当我们拥有比绿色线程更多的 goroutine 时,调度程序处理可用线程间 goroutine的分布,并确保当这些 goroutine 被阻塞时,可以运行其他 goroutine。
Go遵循称为 fork-join 模型的并发模型.fork这个词指的是在程序中的任何一点,它都可以将一个子执行的分支分离出来,以便与其父代同时运行。join这个词指的是这样一个事实,即在将来的某个时候,这些并发的执行分支将重新组合在一起。子分支重新加入的地方称为连接点。这里有一个图形表示来帮助你理解它:
go关键字为Go程序实现了fork,fork的执行者是 goroutine。sayHello()
函数会在属于它的 goroutine 上运行,与此同时程序的其他部分继续执行。在这个例子中,没有连接点。执行 sayHello()
的goroutine将在未来某个不确定的时间退出,并且该程序的其余部分将继续执行。
然而,这个例子存在一个问题:我们不确定 sayHello()
函数是否可以运行。goroutine将被创建并交由Go的运行时安排执行,但在 main()
goroutine退出前它实际上可能没有机会运行。
这里
main()
函数会在sayHello()
之前执行完成,所有不会看到Println输出信息。
这种情况,可以加入连接点确保程序正确性并消除竞争条件。创建连接点可以通过多种方式完成,这里使用sync包中提供的一个解决方案:sync.WaitGroup (后面并发原语文章会详细介绍,这里简单了解即可) 。
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func sayHello() {
fmt.Println("hello world")
wg.Done()
}
func main() {
wg.Add(1)
go sayHello()
wg.Wait()
fmt.Println("done")
}
主goroutine的运作
封装 main()
函数的 goroutine 称为主 goroutine。 主 goroutine 会由 runtime.m0
负责运行。
主 goroutine 所做的事情不是执行 main()
函数那么简单。它首先要做:设定每一个 goroutine 所能申请的栈空间的最大尺寸。 在 32 位的计算机系统中此最大尺寸为 250 MB , 而在 64 位的计算机系统中此尺寸为 1 GB。如果有某个 goroutine 的栈空间尺寸大于这个限制,那么运行时系统就会发起一个栈溢出( stack overflow )的运行时恐慌。 随即,这个Go程序运行也会终止。
在设定好 goroutine 的最大栈尺寸之后, 主 goroutine 会在当前 M 的 g0
上执行系统检测任务。其作用就是为调度器查漏补缺,也是让系统检测任务的执行先于 main()
函数。
此后,主 goroutine
会进行一系列的初始化工作
- 检查当前 M 是否是
runtime.m0
。 如果不是,就说明之前的程序出现了某种问题;这时主 goroutine 会立即抛出异常,意味着Go 程序启动失败。 - 创建一个特殊的 defer语句,用于在主 goroutine 退出时做必要的善后处理。 因为主 goroutine 也可能非正常的结束。
- 启用专用于在后台清扫内存垃圾的 goroutine,并设置 GC 可用的标识。
- 执行
main
包中的init()
函数。
如果上述初始化工作成功完成,那么主 goroutine 就会去执行 main()
函数。 在执行完 main()
函数之后,它还会检查主 goroutine 是否引发了运行时恐慌,并进行必要的处理。 最后,主 goroutine 会结束自己以及当前进程的运行。