Go语言基于共享变量的并发

309 阅读4分钟

并发概述

在一个线性(即只有一个goroutine)的程序中,程序的执行顺序只由程序的逻辑来决定。在有两个或者更多goroutine的程序中,每一个goroutine内的语句也是按照既定的顺序去执行的,但我们没法知道不同goroutine中的事件的执行顺序,当我们无法确认一个事件是在另一个事件前面或后面发生的话,说明这些事件是并发的。

一个函数在线性的程序中能正常工作,如果在并发情况下,这个函数依然可以正常的工作,我们就说这个函数是并发安全的,并发安全的函数不需要额外的同步工作。我们可以把这个概念概括为一个特定类型的一些方法和操作函数,如果这个类型是并发安全的话,那么所有它的访问方法和操作就都是并发安全的。

sync.Mutex互斥锁

在并发的模型中,我们会用互斥锁来处理并发的问题,锁本身可以认为是一个共享变量,一个线程对锁变量加了互斥锁后,其他线程只能等待,直到锁被释放才能获得锁。在go语言中sync包里的Mutex类型直接支持互斥锁,Lock方法可以获取到token(这里叫锁),并且Unlock方法会释放这个token。

package main

import (
    "fmt"
    "sync"
    "strconv"
)

var balance int
var mu sync.Mutex

/**
 * WaitGroup有三个方法:Add(delta int) / Done() / Wait()
 * Add  创建goroutine时调用wg.Add(1),也可以在创建多个goroutine前调用wg.Add(n)
 * Done 在每个goroutine完成任务后,调用wg.Done(),等同于wg.Add(-1)
 * Wait 在等待所有goroutine的地方调用wg.Wait(),它会阻塞主线程,
 *      当所有goroutine都调用完wg.Done()之后它会返回
 */
var wg sync.WaitGroup

func Deposit(amount int)  {
    mu.Lock() //acquire token
    balance = balance + amount
    fmt.Println("存入金额:" + strconv.Itoa(amount))
    mu.Unlock() //release token
    wg.Done()
}

func Balance() int {
    mu.Lock()
    //defer用来执行最后的go语句,一般用于资源释放、关闭连接等操作,会在函数关闭前调用
    //多个defer的定义与执行类似于栈的操作:先进后出,最先定义的最后执行。
    defer mu.Unlock()
    return balance
}

func main()  {
    wg.Add(3)
    //启动3个goroutine,无法判断其执行的先后顺序
    //使用了互斥锁避免多协程竞争,该函数是线程安全的
    go Deposit(100)
    go Deposit(200)
    go Deposit(300)
    wg.Wait()

    fmt.Println(Balance())
}

sync.RWMutex读写锁

package main

import (
    "fmt"
    "sync"
    "strconv"
)

var stock int = 10
var mu sync.Mutex
var wg sync.WaitGroup

//消耗库存
func Destocking() {
    if GetStock() > 0 {
        mu.Lock()
        if stock > 0 {
            stock--
            fmt.Println("库存剩余:" + strconv.Itoa(stock))
        } else {
            fmt.Println("1.已没有库存")
        }
        mu.Unlock()
    } else {
        fmt.Println("2.已没有库存")
    }
    wg.Done()
}

//获取当前库存
func GetStock() int {
    mu.Lock()
    defer mu.Unlock()
    return stock
}

func main() {
    for i := 0; i < 20; i++ {
        wg.Add(1)
        go Destocking()
    }
    wg.Wait()
}

上述程序中,可以看到GetStock函数是获取当前库存,只有读操作并且使用的是互斥锁,当读取的时候会阻塞其它goroutine运行。因为是读取操作,即使多个goroutine同时读取也是安全的,只要在运行的时候没有写操作即可。这时我们需要一种特殊的锁,其允许多个只读操作并行执行,但写操作会完全互斥。这种锁叫做“多读单写”,go语言提供这样的锁是sync.RWMutex。

var mu sync.RWMutex

//获取当前库存
func GetStock() int {
    mu.RLock()
    defer mu.RUnlock()
    return stock
}

这样将GetStock函数换成读锁后,多个goroutine就可以同时读取。

sync.Mutex和sync.RWMutex的区别

go的sync包实现了两种锁Mutex(互斥锁)和RWMutex(读写锁),其中RWMutex是基于Mutex实现的,只读锁的实现使用类似引用计数器的功能。

Mutex是互斥锁,Lock()加锁,Unlock()解锁,使用Lock()加锁后,便不能再次对其进行加锁,直到利用Unlock()解锁对其解锁后,才能再次加锁。适用于读写不确定场景,即读写次数没有明显的区别,并且只允许只有一个读或者写的场景,所以该锁也叫做全局锁。

RWMutex是读写锁,该锁可以加多个读锁或者一个写锁,其经常用于读次数远远多于写次数的场景。除了提供Lock()和Unlock()外,还提供了RLock()和RUnlock()。RLock()读锁,当有写锁时,无法加读锁,当只有读锁或者没有锁时,可以加读锁,读锁可以加多个,所以适用于"读多写少"的场景。RUnlock()读锁解锁,RUnlock撤销单次RLock调用,它对于其它同时存在的读锁则没有效果。