Go语言并发模型——竞争状态

       如果两个或者多个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继续运行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值