解析golang中的并发安全和锁问题

并发
本文我们来学习一下golang中的并发安全和锁问题,文章通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧

1. 并发安全

package main
 
import (
    "fmt"
    "sync"
)
 
var (
    sum int
    wg sync.WaitGroup
)
 
func test() {
    for i := 0; i < 5000000; i++ {
        sum += 1
    }
    wg.Done()
}
 
func main() {
    // 并发和安全锁
    wg.Add(2)
    go test()
    go test()
 
    wg.Wait()
    fmt.Println(sum)
 
}

  上面的代码中我们开启了两个goroutine去累加变量x的值,这两个goroutine在访问和修改x变量的时候就会存在数据竞争,导致最后的结果与期待的不符。

2. 互斥锁

package main
 
import (
    "fmt"
    "sync"
)
 
var (
    sum int
    wg sync.WaitGroup
    mu sync.Mutex  // 定义一个互斥锁
)
 
func test() {
    for i := 0; i < 10000000; i++ {
        // 互斥锁它能够保证同时只能有一个goroutine去访问共享资源
        mu.Lock()
        sum += 1
        mu.Unlock()
    }
    wg.Done()
}
 
func main() {
    fmt.Println(mu)
    // 并发和安全锁
    wg.Add(2)
    go test()
    go test()
 
    wg.Wait()
    fmt.Println(sum)
 
}

  使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其他的goroutine则在等待锁;当互斥锁释放后,等待的goroutine才可以获取锁进入临界区,多个goroutine同时等待一个锁时,唤醒的策略是随机的。

3. 读写互斥锁

互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync包中的RWMutex类型。

读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。

package main
 
import (
    "fmt"
    "sync"
    "time"
)
 
var (
    x int
    wg sync.WaitGroup
    mu sync.Mutex  // 定义一个互斥锁
    rw sync.RWMutex  // 定义一个读写锁,注意:只有读多写少的时候,读写锁才能发挥其优势
)
 
func write() {
    rw.Lock()
    x += 1
    time.Sleep(10 * time.Millisecond)  // 假设写入时间耗费10毫秒
    rw.Unlock()
    wg.Done()
}
func read() {
    rw.RLock()
    time.Sleep(time.Millisecond)
    rw.RUnlock()
    wg.Done()
}
 
func main() {
    start := time.Now()
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go write()
    }  // 写耗时:160毫秒左右
 
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go read()
    }  // 读耗时:15毫秒左右
 
    wg.Wait()
    end := time.Now()
    fmt.Println("执行时间:", end.Sub(start))
}

  需要注意的是读写锁非常适合读多写少的场景,如果读和写的操作差别不大,读写锁的优势就发挥不出来。