Goroutine与锁
在进行并发编程时,很多时候都需要涉及到变量的共享,下面这段代码创建了2个Goroutine来访问变量a并对a进行自加操作,a预期结果应为200000
goroutine-without-lock.go
package main
import (
"fmt"
"sync"
)
func main() {
a := 0
var n sync.WaitGroup
for i := 0; i < 2; i++ {
n.Add(1)
go func() {
defer n.Done()
for j := 0; j < 100000; j++ {
a++
}
}()
} //创建2个Goroutine
n.Wait()
fmt.Printf("a = %d\n", a)
}
大多数情况下并不能得到正确的结果
假设此时a的值为10000,那么在Goroutine [1]读取a的值后,并且未在内存中写入a加1之后的值的这段时间,如果Goroutine [2]此时访问a的值,它得到的值为10000,而不是10001,这样就造成了Goroutine [2]并没有读取到Goroutine [1]更新后的数值,因此会出现少加的情况
解决该问题的办法就是当Goroutine涉及到有关变量a的执行语句时,要确保读取和写入操作完成后其它Goroutine才能访问变量a
goroutine-with-lock.go
package main
import (
"fmt"
"sync"
)
func main() {
a := 0
var mu sync.Mutex
var n sync.WaitGroup
for i := 0; i < 2; i++ {
n.Add(1)
go func() {
defer n.Done()
for j := 0; j < 100000; j++ {
mu.Lock()
a++
mu.Unlock()
}
}()
}
n.Wait()
fmt.Printf("a = %d\n", a)
}
每当Goroutine执行到涉及变量a的语句时,先申请锁,更新完a的值后在释放锁,当一个Goroutine持有锁时,其它的Goroutine都会等待锁释放后再执行申请锁的操作,这样就保证了每次只有一个Goroutine执行变量a的读取和写入操作
加锁的技巧
为了避免程序运行时不必要的等待,在加锁时需要注意仅当Goroutine需要更改共享变量的值时再获取锁,更改完共享变量的值立刻释放锁
一个不恰当的例子
lock-whole-goroutine-execution_time.go
package main
import (
"fmt"
"sync"
"time"
)
const (
UNASSIGN = 0 //未分配
COMPLETED = 1 //执行完毕
)
func main() {
task := make([]int, 10)
var mu sync.Mutex
var n sync.WaitGroup
for index := range task {
task[index] = UNASSIGN
}
for index := range task {
n.Add(1)
go func(index int) {
defer n.Done()
mu.Lock()
time.Sleep(1 * time.Second)
task[index] = COMPLETED
mu.Unlock() //在整个Goroutine加锁
}(index)
}
n.Wait()
fmt.Printf("All task done!\n")
}
在这个例子中,由于在整个Goroutine执行语句进行加锁,导致整个程序执行了10s,结果和串行执行所需时间一样
如果在Goroutine中等待任务完成后(time.Sleep(1 * time.Second)
在此处相当于执行任务,通常情况下任务可以为I/O读写,爬虫请求等等),再申请锁会极大的加快程序执行效率,将上面的代码time.Sleep(1 * time.Second)
与mu.Lock()
互换位置再执行,整个程序仅需1s就可以执行完毕
上述代码其实不适用sync.WaitGroup
也可以实现等待操作,主要思想就是通过循环遍历检查所有的任务是否执行完毕,如果所有任务执行完毕退出循环
lock-without-waitgroup.go
package main
import (
"fmt"
"sync"
"time"
)
const (
UNASSIGN = 0 //未分配
COMPLETED = 1 //执行完毕
)
func main() {
task := make([]int, 10)
var mu sync.Mutex
for index := range task {
task[index] = UNASSIGN
}
for index := range task {
go func(index int) {
time.Sleep(1 * time.Second)
mu.Lock()
task[index] = COMPLETED
mu.Unlock()
}(index)
}
for {
taskDone := true
for index := range task {
taskDone = taskDone && (task[index] == COMPLETED)
} //当所有任务执行完毕时,taskDone为true
if taskDone {
break
} //当所有任务执行完毕时,退出循环
}
fmt.Printf("All task done!\n")
}
上面的代码还可以优化一下,由于使用的是for {}
语句,会一直占用CPU,为了避免其一直占用CPU,在for循环内部可以添加time.Sleep(100 * time.Millisecond)
网友评论