GO语言并发和锁

2020-04-11  本文已影响0人  夜凉听风雨

使用goroutine来写一个开启2个协程执行同一个方法的代码。声明一个add函数, 在函数内循环五万次,每次让全局变量x加1。在main函数中,开辟两个goroutine调用add方法。

package main
import (
    "fmt"
    "sync"
)

var x int
var wg sync.WaitGroup

func add() {
    for i := 0; i < 50000; i++ {
        x = x + 1
    }
    wg.Done()
}


func main() {
    wg.Add(2)
    go add()
    go add()
    wg.Wait() // 等待两个协程执行完毕
    fmt.Println(x)
}

预期: 按照正常思维两个add函数同时运行,每个函数让x加运算五万次,最后两个goroutine运行完毕,结果应该是打印x为10万。

结果: 打印结果远远不到10万

原因: 多个协程同时争抢同一个资源。

分析: 比如当x=100的时候,此时第一个协程正在执行x = x + 1的,x的值为100,而第二个协程也正在执行x = x + 1这一步,这个时候就是两个协程在同时争抢同一个资源x。它们执行完毕,x都为101,相当于少加了一次。所以我们最后得到的x结果远远小于10万。

如何解决这个问题呢?

下面就需要用锁来处理

package main
import (
    "fmt"
    "sync"
)

var x int
var wg sync.WaitGroup
var lock sync.Mutex

func add() {
    for i := 0; i < 50000; i++ {
        lock.Lock() // 上锁
        x = x + 1
        lock.Unlock() // 解锁
    }
    wg.Done()
}


func main() {
    
    wg.Add(2)
    go add()
    go add()
    wg.Wait()
    fmt.Println(x)
}

使用sync.Mutex就可以为代码加上互斥锁。在上述代码中,使用lock.Lock()为x = x + 1这句代码上锁。当一个协程正在执行x = x + 1时,其他协程在执行这句代码之前必须等待,只有当上一个正在执行这句代码的协程执行结束,解锁后其他协程才能有一个协程执行x = x + 1

运行后打印结果为100000,符合预期。

互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync包中的RWMutex类型。
读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。

总结一下:
1.同时只能有一个goroutine 获取写锁
2.同时可以有多个goroutine 获取读锁
3.同时只能有多个读锁或者只有一个写锁
写锁的时候,其他协程啥也不干,当读锁的时候其他协程也可以去读锁但不能去写锁

package main
import (
    "fmt"
    "sync"
    "time"
)

var (
    x int
    y int
    wg sync.WaitGroup
    lock sync.Mutex
)

func write() {
    wg.Add(1)
    lock.Lock()
    x = x + 1
    time.Sleep(time.Millisecond * 1)
    lock.Unlock()
    wg.Done()
}


func read() {
    wg.Add(1)
    lock.Lock()
    y = x
    time.Sleep(time.Millisecond * 1)
    lock.Unlock()
    wg.Done()
}

func main() {
    
    start := time.Now()
    
    for i := 0; i < 1000; i++ {
        go write()
    }

    for i := 0; i < 10000; i++ {
        go read()
    }
    
    wg.Wait()
    end := time.Now()
    fmt.Println(end.Sub(start))
    fmt.Println(x,y)
}

互斥锁打印结果.png
package main
import (
    "fmt"
    "sync"
    "time"
)

var (
    x int
    y int
    wg sync.WaitGroup
    lock sync.RWMutex
)

func write() {
    wg.Add(1)
    lock.Lock()
    x = x + 1
    time.Sleep(time.Millisecond * 1)
    lock.Unlock()
    wg.Done()
}


func read() {
    wg.Add(1)
    lock.RLock()
    y = x
    time.Sleep(time.Millisecond * 1)
    lock.RUnlock()
    wg.Done()
}

func main() {
    
    start := time.Now()
    
    for i := 0; i < 1000; i++ {
        go write()
    }

    for i := 0; i < 10000; i++ {
        go read()
    }
    
    wg.Wait()
    end := time.Now()
    fmt.Println(end.Sub(start))
    fmt.Println(x,y)
}

读写互斥锁打印结果.png

对比上面两份代码和打印结果,可以发现:互斥锁耗时更长,读写互斥锁耗时更短。同时执行读操作的协程很早就完成了,同时执行写操作的协程则更晚才完成。因为读操作的协程可以同时执行不需要相互等待,而写操作的协程必须等待上一个写操作的协程或者上一个读操作的协程执行完毕才能执行。

上一篇 下一篇

猜你喜欢

热点阅读