Go保证并发安全底层实现详解
什么是并发安全
并发安全是指在多线程/多协程同时访问共享变量时,不会出现数据的不一致、不完整、未定义行为等问题。在多核CPU等多核心系统中,我们通常会采用并发编程的方式提高程序的性能,但是多线程/多协程的并发访问也会引发一些并发安全的问题。因此,为了保证程序的正确执行,我们需要确保程序在并发访问共享变量时仍然保持正确性,这就需要实现并发安全。
Go语言的并发安全性
Go语言天生的并发支持让它在并发编程方面有着非常好的表现。在编写支持并发的代码时,我们通常会使用goroutine来完成并发操作,而通常我们需要实现的并发安全主要包括两个方面:
-
互斥锁(mutex):当并发访问共享变量时需要确保各个goroutine之间互斥进行。这个时候,Go语言提供了sync包中的Mutex类型,它本质上是通过底层的互斥锁机制来实现的。
-
原子操作:另一个解决并发安全的方法是通过原子操作来实现。原子操作是指在不需要锁的情况下,保证数据的操作是“原子”的,即操作不会被中断,保证操作的完整性。Go语言提供了sync/atomic包用于提供原子操作的支持。
互斥锁的底层实现
互斥锁的底层实现使用了sync包中的Mutex类型。例如我们定义了一个数值类型变量x,多个goroutine需要访问这个变量,我们可以这样定义:
import "sync"
var x int
var mtx sync.Mutex
func addOne() {
mtx.Lock()
x++
mtx.Unlock()
}
在上面的代码中,当多个goroutine同时访问addOne函数时,它们之间便会被互斥锁机制接管。当第一个goroutine访问addOne时,它会通过调用mtx.Lock()方法获取到互斥锁,此时其他goroutine会被阻塞。当第一个goroutine执行完毕并调用mtx.Unlock()后,其他阻塞的goroutine才能继续执行。
互斥锁的实现中,当一个goroutine持有锁时,其他goroutine试图获取锁的操作会被阻塞,这很容易导致死锁的情况。因此,在使用互斥锁时需要尽可能避免死锁的情况。
原子操作的底层实现
原子操作是不需要锁的,因此,原子操作的性能比互斥锁更高。Go语言中提供了sync/atomic包,用于提供原子操作的支持。
例如我们定义一个数值类型变量x,多个goroutine需要访问这个变量,我们可以这样使用原子操作:
import "sync"
var x int32
func addOne() {
atomic.AddInt32(&x, 1)
}
在上面的代码中,我们使用了sync/atomic包的AddInt32方法来对变量x进行原子加1操作。
原子操作底层的实现方式实际上是通过CPU的CAS(Compare-and-Swap)指令来完成的。当多个goroutine同时访问一个变量时,它们会先读取这个变量的值,然后对这个值进行修改,最后再将修改后的值写回共享变量。在修改共享变量的时候,使用CAS指令可以保证在这个过程中不会被其他goroutine干扰,因此可以确保原子操作的正确性。
示例说明
示例1
下面的示例展示了如何使用互斥锁实现并发访问。代码中我们通过计算斐波那契数列来模拟多个goroutine之间的并发访问。
package main
import (
"fmt"
"sync"
)
var mtx sync.Mutex
func fib(n int) int {
if n <= 1 {
return 1
}
return fib(n-1) + fib(n-2)
}
func calc(n int, ch chan<- int) {
mtx.Lock()
ch <- fib(n)
mtx.Unlock()
}
func main() {
chs := make([]chan int, 10)
for i := 0; i < 10; i++ {
chs[i] = make(chan int)
}
for i := 0; i < 10; i++ {
go calc(i, chs[i])
}
for i := 0; i < 10; i++ {
fmt.Printf("fib(%d) = %d\n", i, <-chs[i])
}
}
在上面的代码中,我们定义了一个fib函数用于计算第n项斐波那契数列的值。然后我们定义了一个calc函数,它的作用是通过互斥锁访问fib函数。在main函数中,我们创建了10个goroutine来计算斐波那契数列的值,并将结果输出。
示例2
下面的示例展示了如何使用原子操作实现并发访问。代码中我们通过计算斐波那契数列来模拟多个goroutine之间的并发访问。
package main
import (
"fmt"
"sync/atomic"
)
var x int32
func fib(n int) int {
if n <= 1 {
return 1
}
return fib(n-1) + fib(n-2)
}
func calc(n int, ch chan<- int) {
res := fib(n)
atomic.AddInt32(&x, int32(res))
ch <- res
}
func main() {
chs := make([]chan int, 10)
for i := 0; i < 10; i++ {
chs[i] = make(chan int)
}
for i := 0; i < 10; i++ {
go calc(i, chs[i])
}
for i := 0; i < 10; i++ {
<-chs[i]
}
fmt.Printf("sum of fib = %d\n", x)
}
在上面的代码中,我们定义了一个fib函数用于计算第n项斐波那契数列的值。然后我们定义了一个calc函数,它的作用是通过原子操作访问fib函数,并将计算结果累加到x变量中。在main函数中,我们创建了10个goroutine并行计算斐波那契数列的值,并计算它们的和。最终输出的结果就是斐波那契数列的和。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Go保证并发安全底层实现详解 - Python技术站