Golang并发实践:使用共享变量实现并发
goroutine与线程
可增长的栈
每个OS都有一个固定大小的栈内存(通常为2MB);在函数中定义的一些基本类型的变量和对象的引用变量都在栈内存中分配;
goroutine在生命周期开始时只有一个很小的栈,典型情况下为2KB; goroutine的栈不是固定大小的,可以按需增大和缩小,goroutine的栈大小限制可以达到1GB;
goroutine的调度
OS线程由OS内核来调度;每隔一定时间,一个硬件时钟中断发送到CPU,CPU调用一个叫调度器的内核函数; 这个函数暂停当前正在运行的线程,把寄存器信息保存到内存,查看线程列表并决定接下来运行哪一个线程; 再从内存恢复线程的注册表信息,最后执行选中的线程;
因为OS由内核来调度,所以控制权从一个线程到另外一个线程需要一个完整的上下文切换(context switch): 即保存一个线程的状态到内存,再恢复另外一个线程的状态;最后更新调度器的数据结构;
考虑这个操作涉及的内存区域性以及涉及的内存访问数量,还有访问内存所需的CPU周期数量的增加,这个操作是很慢的;
Go runtime包含一个自己的调度器,这个调度器使用M:N调度的技术(调度m个goroutine到n个OS线程); Go调度器与内核调度器的工作类似,但Go调度器只需关心单个Go程序的gouroutine的调度问题;
Go调度器是由特定的Go语言结构来触发的,比如当一个goroutine调用time.sleep或被通道阻塞或对互斥量操作时,调度器会将这个goroutine设为休眠模式; 并运行其他goroutine直到前一个可重新唤醒为止; 因为它不需要切换到内核语境,所以调用一个goroutine比调度一个线程成本低很多;
GOMAXPROCS
go调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行GO代码;默认是机器上的CPU数量; 所以在一个8个CPU的机器上,调度器会把Go代码同时调度到8个OS线程上;
正在休眠或者正被通道阻塞的goroutine不需要占用线程;
阻塞在IO和其他系统调用中或者调用非Go语言写的函数的goroutine需要一个独立的OS线程;但这个线程不记在GOMAXPROCS内;
for {
go fmt.Print(0)
fmt.Print(1)
//time.Sleep(5*time.Millisecond)
}
GOMAXPOCS=2 go run main.go 11111111111111111111111111111111111111111111111110000000000000000000000000……
GOMAXPOCS=1 go run main.go 10101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010……
goroutine没有标识
goroutine没有可供访问的标识; 线程局部存储有一种被滥用的倾向;
竞态
concurrency-safe:
考虑一个能在串行程序中正确工作的函数,如果这个函数在并发调用时仍然能正确工作,那么,这个函数是并发安全的;
如果一个类型的所有可访问方法和操作都是并发安全的,就是并发安全的类型;
竞态:
在多个goroutine按某些交错顺序执行时,程序无法给出正确的结果;
var (
balance int
)
func Deposit0(amount int) {
balance += amount
}
func Balance0() int {
return balance
}
互斥锁:sync.Mutex
Go语言的互斥量不可再入
import (
"fmt"
"sync"
"time"
)
var (
sema = make(chan struct{}, 1)
mu sync.Mutex
balance int
)
// 使用容量为1的通道来保证同一时间最多有一个goroutinue能访问共享变量
func Deposit(amount int) {
sema <- struct{}{}
balance += amount
<-sema
}
func Balance() int {
sema <- struct{}{}
b := balance
<-sema
return b
}
//使用sync.Mutex
func Deposit1(amount int) {
mu.Lock()
balance += amount
mu.Unlock()
}
func Balance1() int {
mu.Lock()
defer mu.Unlock()
return balance
}
读写互斥锁:sync.RWMutex
func Balance1() int {
mu.RLock()
defer mu.RUnlock()
return balance
}
内存同步
延迟初始化:sync.Once
竞态检测:-race
把-race命令行参数加到go build、go run、go test命令里面就可以使用该功能;
竞态检查报告所有实际运行了的数据竞态;只能检测到那些在运行时发生的竞态,无法保证肯定不会发生竞态;所以需要确保测试包含了并发使用包的场景;
go版本
go version go1.13.6 darwin/amd64
refs
《The Go Programming Language》Alan A.A, Donovan & Brian W.Kernighan