Go语言编程基础:竞态

172 阅读3分钟
//竞态

竞态指多个goroutine按某些交错顺序执行时程序无法给出正确结果。
因为我们无法知道一个goroutine中的事件x和另一个goroutine中的事件y
的执行先后顺序。它们是并发的。

    package main

    import (
            "fmt"
            "time"
    )

    var myMoeny int

    func give(money int) {
            myMoeny += money
    }

    func look() int {
            return myMoeny
    }

    func main() {

            go func() {
                    give(100)
                    fmt.Println("1:", look())
            }()

            go func() {
                    give(200)
                    fmt.Println("2:", look())
            }()

            time.Sleep(time.Second * 2)

    }
这个时候由于两次存钱同时进行(不知道先后),有可能就出现
1:300 , 2:200的情况。但这并不是我们的初衷,我们希望的是
11002300的情况。

上面的例子中出现了数据竞态的情况:
两个goroutine并发读写一个变量并且至少其中一个是写入时。

根据数据竞态的定义我们有如下解决办法:
1. 在其他goroutine创建之前,数据已经初始化且不在修改,
    这样每个gouroutine都只读,就不会出现数据竞态。
    
2. 避免从多个goroutine访问同一个变量。
    由于其他goroutine无法直接访问变量,它们需要使用
    通道来发送查询请求或者更新变量。(即不要通过共享
    内存来通信,通过通信来共享内存)。
    package main

    import (
            "fmt"
            "time"
    )

    var giveMoney = make(chan int)
    var recvMoney = make(chan int)

    func give(money int) {
            giveMoney <-money
    }

    func look() int {
            return <-recvMoney
    }

    func teller() {
            var myMoney int

            for {
                    select {
			case m := <-giveMoney:
				myMoney += m
			case recvMoney<- myMoney:
                    }
            }
    }

    func main() {

            go teller()

            give(100)
            fmt.Println("1:", look())

            give(200)
            fmt.Println("2:", look())

            time.Sleep(time.Second * 2)

    }

3. 运行多个gotoutine访问一个变量,但同一时间只能有一个访问。
互斥锁:sync.Mutex
    package main

    import (
            "fmt"
            "sync"
            "time"
    )

    var (
            mu sync.Mutex //保护共享变量myMoney
            myMoney int
    )

    func give(money int) {
            mu.Lock()
            defer mu.Unlock()

            myMoney += money
    }

    func look() int {
            mu.Lock()
            defer mu.Unlock()

            return myMoney
    }


    func main() {
            give(200)
            fmt.Println("1:", look())

            go func() {
                    give(100)
                    fmt.Println("2:", look())
            }()
            
            time.Sleep(time.Microsecond * 1000)
    }
通过Lock方法获取一个互斥锁,如果其他goroutine取走互斥锁,那么操作
会一直阻塞到其他goroutine调用Unlock之后。


读写互斥锁:sync.RWMutex
它允许读操作并发执行,写操作只能独享访问权限
    var (
            mu sync.RWMutex //保护共享变量myMoney
            myMoney int
    )
    
    func give(money int) {
            mu.Lock() //写锁
            defer mu.Unlock()

            myMoney += money
    }

    func look() int {
            mu.RLock()//读锁
            defer mu.RUnlock()

            return myMoney
    }
    sync.RWMutex在符锁竞争激烈的时候才有优势,不激烈时比普通互斥锁慢。
    
//goroutine与线程

goroutine并不等同于操作系统中的线程
1.操作系统中每个线程都有一个固定大小的栈内存,用来保护正在执行或者
    临时暂停的函数中的局部变量。
  goroutine的栈时可增长的,最开始时很小,最大可到1GB
2.OS线程由OS内核调度。每隔几毫秒,一个硬件时钟中断发到CPU,CPU调用
    一个叫调度器的内核函数。
  Go运行时包含一个自己的调度器,它可以调m个进程到n个OS线程。Go调度器
      不是由硬件时钟定期触发的,是由Go语言结构出发的。消耗成本更低。
3.Go调度器使用GOMAXPROCS参数确定使用多少个OS线程来同时执行Go代码。
    默认值是机器上的CPU数量。
4.goroutine没有标识符。OS线程都有独特的标识符。