Go语言中goroutines共享内存。这对性能有好处,但是从多个goroutine修改相同的内存是不安全的, 会导致数据争用和崩溃。这是Go的座右铭:不要通过共享内存进行通信;而是通过通信共享内存。
确保goroutine独占访问数据的另一种方法是使用互斥体。
var cache map[int]int
var mu sync.Mutex
func expensiveOperation(n int) int {
// in real code this operation would be very expensive
return n * n
}
func getCached(n int) int {
mu.Lock()
v, isCached := cache[n]
mu.Unlock()
if isCached {
return v
}
v = expensiveOperation(n)
mu.Lock()
cache[n] = v
mu.Unlock()
return v
}
func accessCache() {
total := 0
for i := 0; i < 5; i++ {
n := getCached(i)
total += n
}
fmt.Printf("total: %d\n", total)
}
cache = make(map[int]int)
go accessCache()
accessCache()
sync.Mutex的初始值是有效的mutex,所以不必初始化。为了提高性能,我们希望最小化持有锁的时间。与许多其他语言不同,Go互斥锁是非递归的。如果相同的goroutine尝试两次对互斥量进行Lock(),则第二个Lock()将永远阻塞。
在sync.Mutex Lock()中始终采用排他锁。
在重读场景中,如果我们允许多个读者但只允许一个写者,则可以提高性能。sync.RWMutex具有两种锁定功能:用于读取的锁定和用于写入的锁定。它遵循以下规则:
不要拷贝互斥锁。sync.Mutex变量的副本以与原始互斥锁相同的状态开始,但不是同一个Mutex。
拷贝互斥锁几乎总是错误的, 比如通过将其传递给另一个函数或将其嵌入结构中并复制该结构。如果要共享互斥变量,请将其作为指针* sync.Mutex传递。
互斥体不是递归的(又名可重入)。在某些语言中,互斥锁是递归的,即同一线程可以多次锁定同一互斥锁。
在Go中sync.Mutex是非递归的。在同一goroutine中两次调用Lock将导致死锁。
如果不使用sync.Mutex来确保goroutine之间的数据独占访问,或者忘记锁定程序的某些部分,则会引起数据争夺。数据争用可能导致内存损坏或崩溃。使用Go可以很容易地通过附加检查来对代码进行检测。
使用-race进行go build或go run。 例如:go run -race data_race.go
go语言本身内置了调度和上下文的切换,开发者只需要关注自己的函数,并且交给goroutine就可以了
多个Goroutine
var wg sync.WaitGroup
func hello(i int) {
defer wg.Done() // 结束后 goroutine的注册数量就会-1
fmt.Println("Hello Goroutine!", i)
}
func main() {
for i := 0; i < 2; i++ {
wg.Add(1) // 注册goroutine
go hello(i) // 启动goroutine
}
wg.Wait() // 等待所有的goroutine完成
}
多个goroutine并发执行,并不能保证顺序,同时main函数结束,协程也就会结束,所以想保障正确的逻辑,就需要通过waitGroup来完成。
java和go中sync包通过共享内存实现通信,go channel提倡通过通信共享内存,这就是典型的CSP思想的实践。
程序在给channel分配内存时,可以指定channel的容量大小。可以根据是否具有容量将channel分为无缓冲和缓冲两类。
// 同步
c1 := make(chan int)
// 异步
c2 := make(chan int,2)
# 代码
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
Goroutine是很轻量级的协程,但是频繁的创建协程也需要很大的开销,主要表现在创建(内存),调度(调度器),删除(GC)。
虽然Goroutine是用户态上的操作,但是最终都需要交给系统线程,而系统线程也有承载压力,所以我们需要协程池来降低这部分的压力。
协程池通过维护一组协程(也就是Goroutine),来处理并发任务。协程池可以有效地控制协程的数量,避免过多的协程导致系统资源的浪费,从而提高程序的性能和稳定性。
实现协程池一般需要以下几个步骤:
2.4 GMP模型
结构图如下:
go func()执行过程