并发状态中最容易发生的问题就是竞争问题,而Go以一种更天生的姿态来处理并发问题
目录
存在问题:两个线程之间竞争,共享资源会出错
package main
import (
"fmt"
"runtime"
"sync"
)
var (
counter int
wg sync.WaitGroup
)
func main() {
wg.Add(2)
// 创建两个gorountine
go inCounter(1)
go inCounter(2)
fmt.Println("waiting for final counter……")
wg.Wait()
fmt.Println("finish final counter! The final counter: ", counter)
}
func inCounter(id int) {
defer wg.Done()
for count := 0; count < 2; count++ {
value := counter //捕获counter的值
runtime.Gosched() //当前gorountine从线程中退出并放回到队列,但不会挂起go进程未来会恢复执行
value++
counter = value
}
}
运行结果:期望结果是4,但是可能出现各种结果,比如 2、3等,为什么呢?因为在一个线程读取counter的时候,还没有进行写操作的时候,这个counter资源已经被另一个线程读取了!
解决方案之原子操作
import "sync/atomic"
atomic包提供了底层的原子级内存操作,对于同步算法的实现很有用。
这些函数必须谨慎地保证正确使用。除了某些特殊的底层应用,使用通道或者sync包的函数/类型实现同步更好。
应通过通信来共享内存,而不通过共享内存实现通信。
利用atomix.AddInt64()进行改进:
package main
import (
"fmt"
"runtime"
"sync"
"sync/atomic"
)
var (
counter int64
wg sync.WaitGroup
)
func main() {
wg.Add(2)
// 创建两个gorountine
go inCounter(1)
go inCounter(2)
fmt.Println("waiting for process over……")
wg.Wait()
fmt.Println("finish final counter! The final counter: ", counter)
}
func inCounter(id int) {
defer wg.Done()
for count := 0; count < 2; count++ {
// 利用原子操作安全地对counter进行+1操作,避免了读后写前资源被其他线程竞争后抢夺
atomic.AddInt64(&counter, 1)
// 从当前gorountine中退出,并放回到队列中
runtime.Gosched()
}
}
解决方案之互斥锁创建临界区
何为临界区?
只能被串行化访问或者执行的某个资源/代码段称为临界区
下面用mutex互斥锁加锁临界区来处处理上面这个问题:
package main
import (
"fmt"
"runtime"
"sync"
)
var (
counter int64
wg sync.WaitGroup
mutex sync.Mutex //定义一段代码临界区
)
func main() {
wg.Add(2)
// 创建两个gorountine
go inCounter(1)
go inCounter(2)
fmt.Println("waiting for process over……")
wg.Wait()
fmt.Println("finish final counter! \n The final counter: ", counter)
}
func inCounter(id int) {
defer wg.Done()
for count := 0; count < 2; count++ {
// 加互斥锁创建临界区
mutex.Lock()
{
value := counter
// 从当前gorountine线程退出并放回到队列中
runtime.Gosched()
// 增加本地value并放回到counter中
value++
counter = value
}
// 解锁释放临界区
mutex.Unlock()
}
}
但是用互斥锁来保护一个数值型共享变量比较麻烦并且效率低下,所以这种情况之下还是使用原子操作比较好。
再举个栗子
加锁操作
package main
import (
"fmt"
"sync"
)
var total struct {
sync.Mutex
value int
}
func worker(wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i <= 100; i++ {
// 保证原子操作,加锁-操作-解锁
total.Lock()
total.value += i
total.Unlock()
}
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
go worker(&wg)
go worker(&wg)
wg.Wait()
fmt.Println(total.value)
}
原子操作
package main
import (
"fmt"
"sync"
"sync/atomic"
)
var total uint64
func worker(wg *sync.WaitGroup) {
defer wg.Done()
var i uint64
for i = 0; i <= 100; i++ {
// 无锁方法,原子操作
atomic.AddUint64(&total, i)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
go worker(&wg)
go worker(&wg)
wg.Wait()
fmt.Println(total)
}
结语和参考
原子操作和互斥锁并不是Golang所特有的并发编程手段,下一篇将讨论一下Go特有的通道方式处理gorountine同步与竞争问题!