这是我参与「第五届青训营 」伴学笔记创作活动的第 10 天
1.复习已学知识
- 复习并发编程的基本知识
- 复习Goroutine
- 复习WaitGroup
2.观看Go语言进阶与依赖管理
- 并发安全 Lock
昨天学习了WaitGroup,让我们将一个经典的并发安全问题,运行如下代码:
var x int64
func addWithoutLock() {
for i := 0; i < 2000 ; i++ {
x += 1
}
}
func main(){
x = 0
for i := 0; i < 5; i++ {
go addWithoutLock()
}
time.Sleep(time.Second)
println("WithoutLock:",x)
}
这是一个很简单的程序,由 5 个协程各自将 x 值 +2000。按理来说,期望的输出结果应该是 10000。
但是运行后,得到结果(并不是每次都会得到这样的结果,如果没能复现建议多试几次):
WithoutLock: 9446
这很奇怪,本应输出 10000 的代码,为什么输出结果始终不足 10000 呢。
这就要扯到并发安全这件事情了。事实上对一个变量进行修改并不是一个 原子操作。原子操作意味着,该操作是一步到位的,但是修改一个变量需要经过取出变量值,修改取出的值,将值存回变量三个步骤,考虑这么一个情况:i 值当前是 1000,当协程 1 将这个值取出时,协程 2 和协程 3 已经将该值取出,修改为 1001 和 1002 并存回 i,这时当协程 1 将他拿到的那个 1000 +1 并存回变量 i 时,变量又变回了 1001,于是就产生了错误。
为了解决该问题,我们引入并发锁 sync.Mutex,修改代码:
var (
x int64
lock sync.Mutex
)
func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock()
x += 1
lock.Unlock()
}
}
func main() {
x = 0
for i := 0; i < 5; i++ {
go addWithLock()
}
time.Sleep(time.Second)
println("WithLock:", x)
}
并发锁的原理是,当第一次调用 Lock 方法时,什么都不会发生,但当第二次调用 Lock 方法时,该调用便会立刻阻塞协程,直到有程序调用 Unlock 方法解锁。
并发锁的使用要十分谨慎,请尽在有并发安全的代码中使用并发锁,因为并发锁的使用实际上将并行的程序串行化,会导致显著降低性能;同时,不当的锁使用也可能导致死锁(DeadLock)等问题发生。
除了采用锁的方式保证并发安全,对于单个值的修改而不是一段代码的执行,更推荐引入标准库中的 sync/atomic 包来实现。其他语言也有类似的机制来确保并发安全,例如 Java 的 AtomicInteger 等类。
- Channel
你是否有想过如何在协程与协程之间进行数据传输和通信?你可能会说,这不是很容易的事吗,直接维护一个数组或者结构体实例之类的,然后不同协程按需取用内部的数据就行啦。但是事实上,这种通过共享内存实现的数据通信是十分危险的,可能会遇到位置的问题。Go 语言建议我们通过其内置的 通道(Channel) 功能来进行数据通信,从而共享内存。
可以通过如下方式声明一个 Channel:
ch := make(chan int)
声明了一个名为 ch 的无缓冲 Channel,并指定 Channel 的传输数据类型为 int;
无缓存 Channel 意味着,一个数据的发送必须等待另一端代码的接收,如果没有人接收发送的数据,那么发送端便会被永远阻塞。
可以通过如下方式声明一个带缓冲区的 Channel:
ch := make(chan int, 3)
声明一个名为 ch 的有三个缓冲区的 Channel,并指定 Channel 的传输数据类型为 int;
这意味着,该 Channel 内可以有最多 3 个数据未被接收方接收,此时,发送方可以直接发送数据而不必收到阻塞,如果超出缓冲区(在本例中为发送第四个数据,且缓冲区被前三个数据占满),则依旧会被阻塞。
可以通过如下方式向 Channel 发送数据:
ch <- v // 假设 v 是一个 int 变量
然后,通过如下方式从 Channel 中接受数据:
v := <-ch // 赋值给 v 变量
可以通过使用 for range 的方式来从一个 Channel 中取出所有数据:
for i := range ch {
fmt.Println(i)
}
for range 将会始终读取一个 Channel 中发送的数据,直到该 Channel 被关闭。
当一个 Channel 被使用完毕,应当调用 close 函数关闭 Channel:
close(ch)
以下代码是一个由三个协程组成的程序:第一个协程负责生成 0-9 的数字并通过 Channel 发送给第二个协程;第二个协程接收收到的数字,并将数字进行平方计算,然后将结果发送给主协程;主协程遍历接收到的结果并输出:
func CalSquare() {
src := make(chan int)
dest := make(chan int, 3)
go func() {
defer close(src)
for i := 0; i < 10; i++ {
src <- i
}
}()
go func() {
defer close(dest)
for i := range src {
dest <- i * i
}
}()
for i := range dest {
println(i)
}
}