线程(Thread)对于语言的重要性不言而喻,每个语言都要“发明”自己最高效的线程库以说明自己的厉害,go 也不能免俗的。但 Go 语言却不认为自己是线程,叫 goroutine? Why? 本文通过案例加深对goroutine 的本质理解,在此基础上介绍 goroutine 编程涉及的内容以及四个编程准则。
如果你只是使用 goroutine,直接将 goroutine 理解为轻量级的线程,配合 chan 做通讯,足以应付多数工程项目。如果你要做线程池、或控制 goroutine ,例如 kill,stop,resume 等典型的线程操作,那你就的深入理解 goroutine 的并发,也需要你具备更多 OS 的知识。 本文会告诉你 kill 等想法是不实际的。
用一个不太确当的比喻,有一个病人得了阑尾炎,你作为医生可能会有如下选择:
你可能期望自己选择第一项,但你必须经过严格、长时间的教育和训练。go 的理念是简单,选择的第三项。这合理吗? 答案是合理!
就线程来说,kill 一个线程的需求是似乎“合理”的。但是 java 的 API 在后面的版本也取消了它(但可用),因为强制线程退出会导致涉及资源的锁异常,导致后续程序难以正常获取资源,除非你能控制所有可能的后果。 go 设计者应该这样思考,既然一般程序员不能控制 kill 线程的后果,与其让它产生不确定后果,不如给个简单、高效、安全的 API。
1.1 进程、线程、协程
进程(Process):在内存中的程序。有自己独立的独占的虚拟 CPU 、虚拟的 Memory、虚拟的 IO devices。
OS 直接支持并调度。进程之间只能通过系统提供的 IO 机制通讯。共享内存(变量)是不可能的!
线程(Thread):轻量级进程。在现代操作系统中,是进程中程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。
一个进程由若干线程组成,它们共享进程的计算、存储、IO资源。因此,程序员必须使用系统提供的同步、消息机制,处理资源的竞争和消息的通讯。
线程有分为两大类:
协程(coroutine/fiber):轻量级线程。 是可以并发执行的函数,由编译或用户指定位置将控制权交给协程调度程序执行的方式。它是非抢占式的,可以避免反复系统调用,还有进程切换造成的开销,给你上几千个逻辑流,也称用户级别线程。
在单线程模式下,协程不需要自己上下文,可以大大减少资源竞争的情况。例如,读写map的项时,不需要锁整个表。在 JavaScript、python等单进程单线程、数据驱动(流式)的应用中,协程比线程更有效率;结合回调函数,更高效的处理 IO 请求。
https://www.zhihu.com/question/20511233
1.2 绿色线程(green thread)
知道这个名词的 java 程序员,一定不一般的。一句话就是可管理、可移植的用户线程。
启动一个 goroutine 仅需要 go func(...)
就 OK 了。Go 的表达这个函数是并发执行的,反正这之后它的命运只能由 runtime 库管理,程序员什么也不能做(失控了),甚至你无法直接知道它什么时候结束。它是 Thread 或 Coroutine ? 反正谷歌不解释,直说 goroutine。goroutine 中写个死循环如何?会向线性那样不影响其他 goroutine 吗?如何 kill 它? 这些都是常见问题。
程序猿的探求精神这时无比强大,以下代码主要来自 如何得到goroutine 的 id?
package main
import (
"fmt"
"runtime"
"strconv"
"strings"
"sync"
)
func GoID() int {
var buf [64]byte
n := runtime.Stack(buf[:], false)
idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0]
id, err := strconv.Atoi(idField)
if err != nil {
panic(fmt.Sprintf("cannot get goroutine id: %v", err))
}
return id
}
func main() {
fmt.Println("main", GoID())
runtime.GOMAXPROCS(4)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
i := i
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(i, GoID())
}()
}
fmt.Println("++++++++++++++")
for i:=1<<10; i>0; i-- {
i++
fmt.Print("/")
i--
}
fmt.Println("")
fmt.Println("++++++++++++++")
wg.Wait()
}
运行它,得到结论:
至于取 goroutine ID 就作为一个必须忘记的小技巧。的确我们没有太多理由需要取 goroutine ID。
goroutine 由 runtime 库管理。 现在任务是修改 runtime.GOMAXPROCS(4)
中的 4 改为 1。
结果变了吗? Why?
关于 goroutine 的原理和未来是没有官方的资料的,也是为未来的修改留下伏笔。但就目前实践情况,goroutine 是成功的。
最靠谱的资料 Analysis of the Go runtime scheduler
M: 操作系统的核心线程。
runtime.GOMAXPROCS(4)
就是设置核心线程的数目
G:goroutine
当 M = 1 时,我们观察到 main.main goroutine 执行时,其他 goroutine 不会得到 M 的执行,直到 main.main 的 wg.wait 而交出执行权。
当 M > 1 时,更多的 goroutine 得到执行,产生并发效果
P: M 执行 G 时的上下文
因此:
参考链接:
* golang的goroutine是如何实现的?
* Golang runtime 浅析
* Is there a better way to stop an infinite goroutine in Go?
4.1 尽可能避免使用 runtime 库的函数和方法
尽管你可以使用以下函数,但不建议你使用。
close(c) 或 select
通知 goroutine 退出time.sleep(1)
更稳健4.2 最常用的类型与库
channel 类型与通讯:
time 库
package main
import (
"fmt"
"time"
)
func main() {
tick := time.Tick(100 * time.Millisecond)
boom := time.After(500 * time.Millisecond)
for {
select {
case t := <-tick:
fmt.Println("tick.", t)
case t := <-boom:
fmt.Println("BOOM!")
return
// default:
// fmt.Println(" .")
// time.Sleep(50 * time.Millisecond)
}
}
}
加 default 和 不加 default 的区别?…
时间事件是 goroutine 的最重要事件源之一!
4.3 集合类型都不是线程安全的
5.1 友好线程调度
友好线程调度的准则:不让一个函数长期占用计算资源
代码模板:
for i:=1<<10; i>0; i-- {
time.sleep(...) // 阻塞该函数
doSomeThingInLittleTime(...)
}
为了将控制权交给调度,你需要以下方法:
5.2 关注资源竞争
在使用 xorm 时,以下两段代码是等价的吗?
engine.Where(...).Get(...)
等价于?
engine.Where(...)
engine.Get(...)
如果一样,则 engine 内部必须保持 Where(…) 执行后的状态,供 Get(…) 使用。这导致 engine 不是线程安全的,即另一个线程会在 Get 前修改该状态。这时就必要一个锁,确保 Where(…) Get(…) 不被打断,而产生了资源竞争。事实上,engine.Where(…) 返回的是 Session类(sqlbuilder),以下代码是线程安全的,其中 sqlBuilder 是线程局部变量。
sqlBuilder := engine.Where(...)
sqlBuilder.Get(...)
为了简化资源竞争的管理,人们提出模板化解决资源冲突的方法(管程):
type Resource struct {
locker Locker
resouce ...
}
func (r CResource) AOperation {
r.locker.Lock()
defer r.locker.Unlocker()
...
}
这样,确保共享资源访问时,只有一个操作在执行。但这样会导致 AOperation 调用 BOperation 死锁,即“不可重入”。怎么解决?
5.3 线程初始化参数必须是 final 的!
即线程的参数要么是一个值,要么是一个 final 的引用,否则程序行为将不可预测。 例如:
for _, req := range requests {
go func(&req)
}
java 或 c++ 在编译时是禁止这样的行为(编译错误)。由于 go 没有 final 机制,所以要特别注意!!!
5.4 构建消息驱动(Date Driven/Event Driven)的 goroutine
目前,go 是默认你是熟悉消息驱动机制的高手的! 或者说,你会使用 Channel,Locker,Cond 解决各种同步、通讯场景。
这是一个大的话题,需要你阅读大量的优秀源代码并做总结。以下只是一些常见模式:
接受数据,让一个线程处理一个数据:
connection := Accept()
ctx := prepareContext(connection)
go messageHandler(ctx, ...)
让线程处理消息循环:
func processer(ch) {
for {
message, ok := <-ch
if !ok {
return
}
...
go messageHandler(...)
}
}
异步(函数或接口回调):
messageHandler(yourFunc())
HTTP Reactive Client 是一个典型的消息(事件)驱动的案例。
练习要求:
如果你看到 java 的 Reactive X 然后思考如何实现,就大错特错了。你必须重新回顾函数是第一类成员的理念!建立函数式并发思维去思考通用的异步并发,其中的关键是并发模式:
7.1 基本编程模式与应用
请阅读:Go语言并发的设计模式和应用场景
要点:
7.2 并发应用模型
请阅读:入门goroutine并发设计模式以及goroutine可视化工具
要点:
7.3 一些常见的应用模式
建议阅读:RabbitMQ Tutorials
它的六个图,如何用 goroutine + channel 实现?
goroutine 是比较难且实用的话题,在理解 goroutine 原理的基础上,进一步了解多线程程序的准则以及消息驱动的编程模式,将是 go 分布式编程的利器!