• 2.16 Go之sync包与锁:限制线程对变量的访问


    2.16 Go之sync包与锁:限制线程对变量的访问

    sync包

    sync包提供了的锁:

    • Mutex:互斥锁

    • RWMutex:读写锁

    为什么需要锁

    sync包中的两个核心方法:

    1. Lock:加锁

    2. 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, "写入结束")
    }
  • 相关阅读:
    HDU1260DP
    HDU1114 背包
    HDU1078记忆化搜索
    HDU1024 最大m子段和
    Codeforces Round #401 (Div. 2) A,B,C,D,E
    HDU3666 差分约束
    HDU1540 区间合并
    HDU3308 线段树(区间合并)
    Codeforces Round #403 (Div. 2) B 三分 C dfs
    HDU1573 线性同余方程(解的个数)
  • 原文地址:https://www.cnblogs.com/JunkingBoy/p/15944388.html
Copyright © 2020-2023  润新知