《Concurrency in Go》
本章节从goroutine入手,讲解go语言的各种并发原语。在讲解完goroutine之后,对于传统的内存同步访问的并发原语:sync包中的Mutex,RWMutex,Cond,Once,WaitGroup,Pool等进行了分析。在此之后着重讲了go语言的另一大特色:channel。在最后,讲解了如何结合channel的语法:select语句。
插一句题外话:这本书的中文版本的翻译就是一坨屎。
Chapter 3:Go’s Concurrency Building Blocks Go 语言并发组件
1. goroutine
goroutine是Golang中最基本的组织单位之一,每个go语言的程序都至少有一个goroutine:main goroutine,它在进程开始时自动创建并且启动。
1.1 什么是goroutine?
简单的说:goroutine是一个并发的函数,可以和别的代码块同时运行(不一定是并行的)。
至于如何使用go
关键字来简单的创建一个goroutine,就不多讲了,看到这个博客的人估计没那么傻。
golang中的goroutine是这个世界上独一无二的东西。它不是OS线程,也不是绿色线程(由语言运行时管理的线程)。有些中文的翻译为轻量线程,但是事实上goroutine is a coroutine
,也就是说goroutine是一个协程。协程是一种非抢占式的特殊线程(进程和线程是抢占式的)。协程不能被中断,但是协程尤多个允许暂停和重新进入的点。
1.2 goroutine的独到之处(和普通协程的区别)
goroutine的独到之处在于它们与golang的运行环境的深度集成。(原文是:What makes goroutines unique to Go are their deep integration with Go’s runtime. 这里的所谓golang的运行环境其实是特指的golang的runtime
| 在中文翻译中为:它们与Go语言运行时的深度集成,这根本就不通顺嘛!)
goroutine定义了自己的暂停的方法和再切入的点。Go语言的runtime会观察goroutine的运行时的行为,并且在阻塞的时候自动挂起它们,然后在不被阻塞的时候再恢复。 在golang的runtime和goroutine的逻辑之间有一种优雅的伙伴一样的关系。
1.3 goroutine怎么实现并发
协程(coroutine)和goroutine都是隐式并发结构,这说明并发并不是协程的属性:必须同时托管多个协程,并且给每个协程一个执行的机会。
Golang的主机托管机制是一个M:N调度器,主要机制就是将M个由程序管理的线程映射到N个OS线程。而M:N调度器可以单独写一个博客了,这里就不再细说。
Golang遵循一个称为fork-join
的并发模型。
- fork是指在程序运行中的任意一点,它可以将执行的子分支和父节点同时运行。
- join这个词是指的是在将来的某个时候,这个并发的执行分支将会合并在一起。
在fork-join模型中,掌握join的点是至关重要的,因为join点是保证程序的正确性和消除竞争条件的关键。而控制join点的关键技术是WaitGroup。
1.4 goroutine中闭包运行的情况
我们在快速创建goroutine的时候往往会选择使用匿名函数来创建,这就牵扯到了闭包中变量的引用问题:闭包可以从创建它们的作用域中获取变量,那么当这个闭包运行的时候,调用这些变量的方式是副本还是引用呢?
举个例子:
var wg sync.WaitGroup
salutation := "hello"
wg.Add(1)
go func() {
defer wg.Done()
salution = "welcome"
}()
wg.Wait()
fmt.Println("Out:", salutation)
我的得到的输出是:
Out: welcome
事实证明,goroutine在它们所创建的相同地址空间内执行。
从另一个角度再进行一个实验:
var wg sync.WaitGroup
for _, salutation := range []string{
"hello", "greetings", "good day"} {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(salutation)
}()
}
wg.Wait()
这个程序我们期望得到的结果是:
hello
greetings
good day
以上的所有可能的排列组合,因为我们都知道并发所带来的竞争条件产生的影响,但是输出却让我们大吃一惊:
good day
good day
good day
当大家看到输出的时候应该已经明白了究竟是怎么回事:在输出之前,salutation就已经完成了迭代。
但是值得注意的一点是,既然迭代已经结束,为什么还能使用salutation的引用呢?这个就和golang的GC有关,golang的GC会小心的把salutation的引用从内存转移到堆,以便能够继续使用。
所以正确的程序应该这样编写:
var wg sync.WaitGroup
for _, saluta