2.16 Go之sync包与锁:限制线程对变量的访问
sync包
sync包提供了的锁:
- 
Mutex:互斥锁
- 
RWMutex:读写锁
为什么需要锁
sync包中的两个核心方法:
- 
Lock:加锁
- 
Unlock:解锁
在并发的情况下,多个线程或协程同时其修改一个变量,使用锁能保证在某一时间内,只有一个协程或线程修改这一变量。
不使用锁时,在并发的情况下可能无法得到想要的结果
非并发场景示例:
package main
import (
    "fmt"
    "time"
)
/*
设计一个并发场景,不使用锁看看结果如何
 */
func main() {
    // 设置一个变量a,使用两个协程对其值进行累加
    var a = 0
    for i := 0; i < 1000; i++ {
        // 开启一个协程
        go func(index int) {
            a += 1
            fmt.Println(a)
        }(i)
    }
    // 每次累加以后协程暂歇两秒
    time.Sleep(time.Second)
}
结果集:
968
966
969
970
971
27
分析:
可以明显看到a的值并不是按顺序递增输出的
原因分析:
协程的执行过程:
- 
从寄存器读取 a 的值; 
- 
然后做加法运算; 
- 
最后写到寄存器。 
按照上面的顺序,假如有一个协程取得a的值为3,然后执行加法运算,此时又有一个协程对a进行取值,得到的值同样是3,最终两个协程的返回结果是相同的。
而锁的概念就是,当一个协程正在处理a时将a锁定,其它协程需要等待该协程处理完成并将a解锁后才能再进行操作,也就是说同时处理a的协程只能有一个,从而避免上面示例中的情况出现。 
互斥锁
上诉情况加一个互斥锁就可以解决。
互斥锁的特点:
一个互斥锁只能同时被一个goroutine锁定,其它goroutine将阻塞直到互斥锁被解锁(重新争抢对互斥锁的锁定)
package main
import (
    "fmt"
    "sync"
    "time"
)
/*
设置一个累加变量
使用互斥锁对变量进行锁定控制
*/
func main() {
    // 累加变量
    var a = 0
    // 锁变量
    var lock sync.Mutex
    // 循环累加变量
    for i := 0; i < 1000; i++ {
        // 开启一个协程
        go func(index int) {
            // 进行加锁操作
            lock.Lock()
            // 最后的处理是释放掉锁
            defer lock.Unlock()
            // 进行累加
            a += 1
            fmt.Printf("goruntine %d, a = %d\n", index, a)
        }(i)
    }
    // 等待一秒结束主程序
    // 确保所有协程执行完成
    time.Sleep(time.Second)
}
结果集:
goruntine 962, a = 964
goruntine 963, a = 965
goruntine 4, a = 966
goruntine 966, a = 967
goruntine 965, a = 968
分析:
可以看到虽然协程存在争抢行为,但是该行为并没有影响到累加变量a的累加过程
互斥锁场景示例:
package main
import (
    "fmt"
    "sync"
    "time"
)
/*
开启两个协程
创建一个结构体对象
使用两个协程对该变量进行修改
 */
func main() {
    // 声明一个结构体对象,里面存放一个值
    ch := make(chan struct{}, 2)
    // 声明一个互斥锁变量
    var l sync.Mutex
    // 协程一为锁定协程
    go func() {
        // 开始锁
        l.Lock()
        // 释放锁
        defer l.Unlock()
        fmt.Println("goroutine1: 我会锁定2s")
        // 休眠两秒
        time.Sleep(time.Second)
        fmt.Println("goroutine1: 一已解锁!")
        ch <- struct{}{}
    }()
    // 协程二为等待争抢协程
    go func() {
        fmt.Println("goroutine: 等待解锁")
        // 上锁
        l.Lock()
        // 最后解锁
        defer l.Unlock()
        fmt.Println("goroutine: 二已解锁!")
        // 最后关闭将变量放回协程
        ch <- struct{}{}
    }()
    // 等待goroutine执行结束
    for i := 0; i < 2; i++ {
        <-ch
    }
}
结果示例:
goroutine: 等待解锁
goroutine1: 我会锁定2s
goroutine1: 一已解锁!
goroutine: 二已解锁!
分析:
注意看两个协程的上锁位置。
协程一是一开始就上锁所以此时是协程二先执行
然后协程二开始上锁此时程序执行了协程一当中的代码
最后执行完毕施放了以后再执行协程二当中的代码
读写锁
读写锁有四种方法:
写的上锁和解锁:
func(*RWMutex) Lock
func(*RWMutex) Unlock
读的上锁和解锁:
func (*RWMutex) Rlock
func (*RWMutex) RUnlock
读写锁的区别:
- 
一个 goroutine获得写锁,其他的任何读锁或者写锁都要阻塞到改写解锁
- 
一个 goroutine获得读锁,其他读锁可以继续锁定,但是写锁不可以
- 
一个或者多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定 
这里的读锁定(RLock)目的其实是告诉写锁定,有很多协程或者进程正在读取数据,写操作需要等它们读(读解锁)完才能进行写(写锁定)
概括:
- 
同时只能有一个 goroutine能够获得写锁定;
- 
同时可以有任意多个 gorouinte获得读锁定;
- 
同时只能存在写锁定或读锁定(读和写互斥) 
示例代码:
package main
import (
    "fmt"
    "math/rand"
    "sync"
)
/*
开启两个协程
一个读
一个写
同时对结构体当中的属性进行操作
体会读写锁的特性
 */
// 声明两个变量,一个int的总数变量,一个读写锁变量
var count int
var rw sync.RWMutex
func main() {
    // 设置结构体变量
    ch := make(chan struct{}, 10)
    // 循环进行读或者写的操作
    for i := 0; i < 5; i++ {
        // 调用read方法
        go read(i, ch)
    }
    // 循环进行写的操作
    for i := 0; i < 5; i++ {
        // 调用write方法
        go write(i, ch)
    }
    // 一次将值放回
    for i := 0; i < 10; i++ {
        <-ch
    }
}
/* 构造读取函数 */
func read(n int, ch chan struct{}) {
    // 设置读锁定
    rw.RLock()
    fmt.Printf("goroutine %d 进入读操作...\n", n)
    v := count
    fmt.Printf("goroutine %d 读取结束,值为:%d\n", n, v)
    // 解锁
    rw.RUnlock()
    // 放回
    ch <- struct{}{}
}
/* 构造写入函数 */
func write(n int, ch chan struct{}) {
    // 设置写锁
    rw.Lock()
    // 开始写入
    fmt.Printf("goroutine %d 进入写操作...\n", n)
    // 设置随机数
    v := rand.Intn(1000)
    fmt.Printf("goroutine %d 写入结束, 新的值为:%d\n", n, v)
    // 释放写锁
    rw.Unlock()
    // 放回
    ch <- struct{}{}
}
特征:
上诉的函数中说明读写互斥
展示多个协程读取同一个变量:
package main
import (
    "sync"
    "time"
)
/*
定义一个读写锁变量
定义一个读取函数
构建多个协程调用该函数
 */
var m *sync.RWMutex
func moreRead(i int) {
    println(i, "开始读取!")
    // 上锁
    m.RLock()
    // 读取
    println(i, "reading")
    // 休眠
    time.Sleep(1*time.Second)
    // 施放读锁
    m.RUnlock()
    println(i, "读取结束!")
}
// 调用多个读取的函数
func main() {
    m = new(sync.RWMutex)
    // 多个开始读取--->读锁不互斥
    go moreRead(1)
    go moreRead(2)
    // 休眠
    time.Sleep(2*time.Second)
}
结果集:
2 开始读取!
2 reading
1 开始读取!
1 reading
1 读取结束!
2 读取结束!
可以观察到读锁之间不互斥
读写互斥,所以写操作开始的时候,读操作必须要等写操作进行完才能继续,不然读操作只能继续等待
package main
import (
"sync"
"time"
)
/*
声明一个读写锁变量
定义一个读函数
定义一个写函数
每个函数都是先开始进行读取(写入)
上锁
读取(写入)内容
释放锁
打印结果
*/
var variableM *sync.RWMutex
// 实际调用
func main() {
variableM = new(sync.RWMutex)
// 写入的时候与读取或者写入互斥
go writing(1)
go reading(2)
go writing(3)
// 等待时间
time.Sleep(2*time.Second)
}
// 读函数
func reading(i int) {
println(i, "开始读取!")
// 上锁
variableM.RLock()
// 读取内容
println(i, "读取中...")
// 等待
time.Sleep(1*time.Second)
// 释放锁
variableM.RUnlock()
// 打印结果
println(i, "读取结束!")
}
// 写函数
func writing(i int) {
println(i, "开始写入!")
// 上锁
variableM.Lock()
// 写入内容
println(i, "写入中...")
// 等待
time.Sleep(1*time.Second)
// 释放锁
variableM.Unlock()
// 打印结果
println(i, "写入结束")
}
 
                    
                
 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号