如果两个或者多个goroutine在没有互相同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就处于相互竞争的状态,这种情况被称作竞争状态。对一个共享资源的读和写操作必须是原子化的,换句话说,同一时刻只能有一个goroutine对共享资源进行读和写操作。
下面这一个程序将会出现竞争状态:
package main
import (
"fmt"
"runtime"
"sync"
)
var(
counter int
wg sync.WaitGroup
)
func main(){
wg.Add(2)
go incCounter(1)
go incCounter(2)
wg.Wait()
fmt.Println("Final Counter:",counter)
}
func incCounter(id int){
defer wg.Done()
for count:=0;count<2;count++{
value:=counter
runtime.Gosched()//当前goroutine从线程退出,并返回到队列
value++
counter=value
}
}
变量counter会进行4次读和写操作,每个goroutine执行两次。但是,程序终止时,counter变量的值为2。
每个goroutine都会覆盖另一个goroutine的工作。这种覆盖发生在goroutine切换的时候。每个goroutine创造了一个counter变量的副本,之后就切换到另一个goroutine。当这个goroutine再次运行的时候,counter变量的值已经改变了,但是goroutine并没有更新自己的那个副本的值,而是继续使用这个副本的值,用这个值递增,并存回counter变量,结果覆盖了另一个goroutine完成的工作。
其中调用了runtime包的Gosched函数,用于将goroutine从当前线程退出,给其他goroutine运行的机会。在两次操作中间这样做的目的是强制调度器切换两个goroutine,以便让竞争状态的效果变得更明显。
二、锁住共享资源
Go语言提供了传统的同步goroutine的机制,就是对共享资源加锁。如果需要顺序访问一个整型变量或者一段代码,atomic和sync包里的函数提供了很好的解决方案。
1. 原子函数
原子函数能够以很底层的加锁机制来同步访问整型变量和指针。
package main
import (
"fmt"
"runtime"
"sync"
"sync/atomic"
)
var(
counter int64
wg sync.WaitGroup
)
func main(){
wg.Add(2)
go incCounter(1)
go incCounter(2)
wg.Wait() //等待goroutine结束
fmt.Println("Final Counter: ",counter)
}
func incCounter(id int){
defer wg.Done()
for count:=0;count<2;count++{
atomic.AddInt64(&counter,1)//安全的对counter加1
runtime.Gosched()
}
}
使用了atmoic包的AddInt64函数。这个函数会同步整型值的加法,方法是强制同一时刻只能有一个gorountie运行并完成这个加法操作。当goroutine试图去调用任何原子函数时,这些goroutine都会自动根据所引用的变量做同步处理。
另外两个有用的原子函数是LoadInt64和StoreInt64。这两个函数提供了一种安全地读和写一个整型值的方式。下面是代码使用LoadInt64和StoreInt64来创建一个同步标志,这个标志可以向程序里多个goroutine通知某个特殊状态。
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
var(
shutdown int64
wg sync.WaitGroup
)
func main(){
wg.Add(2)
go doWork("A")
go doWork("B")
time.Sleep(1*time.Second)
fmt.Println("Shutdown Now")
atomic.StoreInt64(&shutdown,1)
wg.Wait()
}
func doWork(name string){
defer wg.Done()
for{
fmt.Printf("Doing %s Work\n",name)
time.Sleep(250*time.Millisecond)
if atomic.LoadInt64(&shutdown)==1{
fmt.Printf("Shutting %s Down\n",name)
break
}
}
}
main函数使用StoreInt64函数来安全地修改shutdown变量的值。如果哪个doWork goroutine试图在main函数调用StoreInt64的同时调用LoadInt64函数,那么原子函数会将这些调用互相同步,保证这些操作都是安全的,不会进入竞争状态。
2. 互斥锁
另一种同步访问共享资源的方式是使用互斥锁。互斥锁这个名字来自互斥的概念。互斥锁用于在代码上创建一个临界区,保证同一时间只有一个goroutine可以执行这个临界代码。下面是代码
package main
import (
"fmt"
"runtime"
"sync"
)
var(
counter int64
wg sync.WaitGroup
mutex sync.Mutex
)
func main(){
wg.Add(2)
go incCounter(1)
go incCounter(2)
wg.Wait()
fmt.Printf("Final Counter:%d\n",counter)
}
func incCounter(id int){
defer wg.Done()
for count:=0;count<2;count++{
//同一时刻只允许一个goroutine进入这个临界区
mutex.Lock()
{
value:=counter
runtime.Gosched()
value++
counter=value
}
mutex.Unlock()//释放锁,允许其他正在等待的goroutine进入临界区
}
}
同一时刻只有一个goroutine可以进入临界区。之后,直到调用Unlock函数之后,其他goroutine才能进去临界区。当调用runtime.Gosched函数强制将当前goroutine退出当前线程后,调度器会再次分配这个goroutine继续运行。