GO 之 Goroutine 学习
Goroutine
Goroutine
算是 Go 语言最大的亮点了。在 Go 里面,每一个并发执行的活动的都称为 Goroutine。当开始接触 Goroutine 的时候,可以将它类比为传统的线程,但你绝不能把它当作线程,两者是有本质的区别的。
当你开始写 Go 程序的时候,必须有一个主函数 main
,这个是由主 Goroutine 来调用。新的 goroutine 由关键字 go
来建立。并且 go 关键字支持匿名函数。像这样:
go func() {
// 执行
fmt.Println("this is a goroutine")
}()
就是这么简单,很轻松的创建一个 Goroutine,这样就可以开启你的并发之旅了。
Go 的并发模型
还记得上面讲了的吗?Goroutine 和线程是有本质区别的。那么 Goroutine 到底是什么呢?Goroutine 既不是 OS 线程,也不是绿色线程(用户空间的线程,有运行的语言管理)。它们是更加抽象且轻量级的,称之为协程。协程是非抢占式的且不能被中断的 Goroutine (函数,闭包或者方法),但是协程有多个点,可以暂停或者重新进入。
Goroutine
是由一个名为 M:N 的调度器实现的,他是将 M 个绿色线程映射到 N 个 OS 线程,然后再将 Goroutine 安排到绿色线程。当 Gouroutine 的数量超过了可用的绿色线程的时候,调度程序将处理分布在线程上的 Gotoutine,并且确保这些 Goroutine 被阻塞的时候,其他的 Goroutine 可以运行。这就是 Goroutine 的并发模型。
Fork-Join 模型
Go 语言还遵循这个一个称为 Fork-Join
的模型。fork
指的是在程序中的任意一点,他可以将执行的子分支与自己的父节点同时运行。join
指的是在将来的某个时候,这些分支会合并在一起。下面的示意图:
下面开看一个简单的例子:
func main() {
go func() {
fmt.Println("hello")
}()
// do something
fmt.Println("world")
}
本例中,使用了一个 Goroutine 开启了一个协程运行,主函数则运行其余部分。在这个例子中,其实是没有 Join 点的,因为你不知道这个协程将在哪个时间点退出,这是不确定的。单单就这个例子而言,我们是不确定是否会打印出 “hello world” 的。Goroutine 虽然被创建了,并且在运行时执行,但是实际上很可能在 main goroutine 退出之后执行,你将看不到任何输出。
ps:如果你看到了也不必有任何疑问,因为这是不确定的。
你可以增加一个竞争条件,例如在 main Goroutine 加入 time.Sleep
,但这并不是创建 Join 点,因为你还是并不能保证 goroutine 一定执行,你还是不确定退出时间。Join 点是保证程序的正确性和消除竞争条件的关键。
所以想要创建一个 Join 点,你必须同步 main Goroutine 和 子 goroutine。这可以通过多种方式来实现,比如 Sync
包。下一篇就主要介绍一下这个包。
下面来看一个有意思的现象,关于 Join 点的问题。注意这里将会使用 Sync
包。来看一下下面的两段代码:
无 Join 点
str := "hello"
go func() {
defer wg.Done()
str = "world"
}()
fmt.Println(str)
有 Join 点
var wg sync.WaitGroup
str := "hello"
wg.Add(1)
go func() {
defer wg.Done()
str = "world"
}()
wg.Wait() // 这里就是 join 点
fmt.Println(str)
当你运行第一段的代码的时候,结果在大多数可能下会是 hello
。当然并不能说是一定,因为这是不确定的。除非你加入竞争条件(其实也是不确定的,只不过加大了概率)或者像第二段代码一样加入了 Join
点。使用 Sync
包之后,引入 Join 点的情况下,可以很确定结果是 world
。因为他保证了程序的正确性,以第二段代码为列的话,可以认为我们确定了 str
变量确确实实被重新赋予了新的值。这是一个确定的事件,所以结果也是确定的。
从上面的例子我们还可以看出来一点,就是 goroutine 在他们所创建的相同的地址空间运行(main goroutine 和 子 goroutine 都同时使用了 str),再来看一段代码:
var wg sync.WaitGroup
str := "hello"
for _, str = range []string{"H", "J", "K"} {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(str)
}()
}
wg.Wait()
请思考一下结果是什么?H, J, K
或者是 H, K, J
?? 很多人可能会认为结果会是无序打印三个值。可是结果并不是你想像的那样,结果大多数情况下会是打印三个 K
。虽然上面已经说了 goroutine 运行在相同的地址空间,但是为什么并不会无序的打印出三个值呢?来看一下这整个过程,goroutine 内打印的是 str
的字符串引用,也就是说整个迭代过程中 str
一直是在变的,当我们创建 goroutine 的时候,因为不确定 goroutine 的在什么时间运行,很可能当 goroutine 运行的时候迭代已经结束了。所以这个例子的代码大概率将会看到三个 k
的输出。可以加大数组的长度加以验证 goroutine 的运行的时间是不确定性的证明。那么这里还有一个问题 。
既然迭代结束了, str 的会不会被回收呢?当然从结果来看,GO 语言还是保留了 str 的引用。
那么该如何解决这个问题呢?既然他每次只能引用到迭代处的值。那么就只需要把迭代的值传入就可以了。
var wg sync.WaitGroup
str := "hello"
for _, str = range []string{"H", "J", "K"} {
wg.Add(1)
go func(s string) {
defer wg.Done()
fmt.Println(s)
}(str)
}
wg.Wait()
阅读中有任何建议和疑问欢迎反馈,有不恰当的地方欢迎修正。共同进步
本作品采用《CC 协议》,转载必须注明作者和本文链接