取消
- 一个
goroutine无法直接终止另一个,因为这样会让所有的共享变量状态处于不确定状态。 - 如果在取消事件到来的时候
main函数没有返回,执行一个panic调用,然后允许是将转储程序中所goroutine的栈。
使用共享变量实现并发
-
导出的包级别函数通常可以认为是并发安全的。
-
数据竞态发生于两个
goroutine并发读写同一个变量并且至少其中一个是写入时。 -
有三种方法避免数据竞态
- 不要修改变量
- 避免从多个
goroutine访问同一个变量。 - 避免数据竞态的方法是允许多个
goroutine访问同一个变量,但在同一个时间只有一个goroutine可以访问。
互斥锁:sync.Mutex
一个计数上限为1的信号量称为二进制信号量。互斥锁模式应用非常广泛,所以sync包有一个单独的Mutex类型来支持这种模式。它的Lock方法用于获取令牌(token,此过程也称为上锁),Unlock方法用于释放令牌。
- 在
Lock和Unlock之间的代码,可以自由地读取和修改共享变量,这一部分称为临界区域。 - 这种函数、互斥锁、变量的组合方式称为监控模式。
读写互斥锁:sync.RWMutex
这是一种特殊类型的锁,它允许只读操作可以并发执行,但写操作需要获得完全独享的访问权限。这种锁称为多读单写锁。
- 可以调用
RLock和RUnlock方法来分别获取和释放一个读锁(也称为共享锁)。 - 可以调用
Lock和Unlock来分别获取和释放一个写锁。 - 仅在绝大部分
goroutine都在获取读锁并且锁竞争比较激烈时,RWMutex才比较有优势。因为RWMutex需要更复杂的内部薄记工作,所以竞争不激烈时它比普通的互斥锁慢。
内存同步
现代计算机一般都有多个处理器,每个处理器都有内存的本地缓存。为了提高效率,对内存的写入是缓存在每个处理器中的,只在必要时才刷回内存。像通道通信或者互斥锁操作这样的同步原语都会导致处理器把累积的写操作刷回内存并提交。
延迟初始化:sync.Once
sync包提供了针对一次性初始化问题的特化解决方案:sync.Once。从概念上来讲,Once包含一个布尔变量和一个互斥量,布尔变量记录初始化是否已经完成,互斥量则负责保护这个布尔变量和客户端的数据结构。Once的唯一方法Do以初始化函数作为它的参数。
竞态检测器
golang语言运行时和工具链装备了一个精致并易于使用的动态分析工具:竞态检测器。
- 简单的把
-race命令行参数加到go build、go run、go test命令里即可使用该功能。它会让编译器为你的应用或测试构建一个修改后的版本。 - 这个工具会输出一份报告,包括变量的表示以及读写
goroutine当时的调用栈。
goroutine与线程
- 每个
OS线程都有一个固定大小的栈内存(通常为2MB),栈内存区域用于保存在其他函数调用期间那些正在执行或者临时暂停的函数中的局部变量。 - 作为对比,一个
goroutine在生命周期开始时只有一个很小的栈,典型情况下为2KB。但与OS线程不同的是,goroutine的栈不是固定大小的,它可以按需增大和缩小。goroutine的栈大小限制可以达到1GB。
goroutine调度
OS线程由OS内核来调度。每隔几毫秒,一个硬件时钟中断发到CPU,CPU调用一个叫调度器的内核函数。因为OS线程由内核来调度,所以控制权限从一个线程到另一个线程需要一个完整的上下文切换;即保存一个线程的状态到内存,再恢复另外一个线程的状态,最后更新调度器的数据结构。考虑这个操作涉及到内存居于性以及涉及的内存访问数量,还有访问内存所需的CPU周期数量的增加,这个操作其实很慢的。
golang运行时包含一个自己的调度器,这个调度器使用一个称为m:n调度的技术(因为它可以复用/调度m个goroutine到n个OS线程)。- 与操作系统的线程调度器不同的是,
golang调度器不是由硬件时钟来定期触发的,而是由特定的golang语言结构来触发的。因为它不需要切换到内核语境,所以调用一个goroutine比调度一个线程成本低很多。
GOMAXPROCS
golang调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行golang。默认值时机器上的CPU数量。
goroutine没有标识
在大部分支持多线程的操作系统中,通常都是去一个整数或者指针来表示线程,构建线程的局部存储,本质上就是全局的map。
goroutine没有可供程序员访问的标识。