zoukankan      html  css  js  c++  java
  • Go基础之锁的初识

    当我们的程序就一个线程的时候是不需要用到锁的,但是通常我们实际的代码不会是单个线程的,所有这个时候就需要用到锁了,那么关于锁的使用场景主要涉及到哪些呢?

    • 当我们多个线程在读相同的数据的时候则是需要加锁的
    • 当我们的程序既有读又有写的时候更是需要加锁的
    • 当我们有多个线程在写的时候同样也是需要加锁

    互斥锁

    互斥锁:同一个时刻只有一个线程能够拿到锁

    我们先通过一个例子来演示,如果当多个线程同时更改一个变量,结果会是怎么样
    不加锁版本

    package main
    
    import (
        "sync"
        "fmt"
    )
    
    var (
        //lock sync.Mutex
        count int
        w sync.WaitGroup  //用于等待子线程执行完之后退出
    )
    
    func main() {
        w.Add(1) // 在调用线程前执行w.add
        go func(){
            for i:=0;i<100000;i++{
                count++
            }
            w.Done()  //执行完 执行w.Done
        }()
        for i :=0;i<100000;i++{
            count++
        }
        w.Wait() // 最后执行w.wait等待所有的线程执行完毕
        fmt.Println(count)
    
    }

    当我们运行多次就可以发现,最后的结果基本不可能是我们先看到的:200000
    我们修改代码代码需要加锁保护的地方加上锁,并且这里加的是互斥锁,修改后的代码为:

    package main
    
    import (
        "sync"
        "fmt"
    )
    
    var (
        lock sync.Mutex
        count int
        w sync.WaitGroup  //用于等待子线程执行完之后退出
    )
    
    func main() {
        w.Add(1) // 在调用线程前执行w.add
        go func(){
            for i:=0;i<100000;i++{
                lock.Lock()
                count++
                lock.Unlock()
            }
            w.Done()  //执行完 执行w.Done
        }()
        for i :=0;i<100000;i++{
            lock.Lock()
            count++
            lock.Unlock()
        }
        w.Wait() // 最后执行w.wait等待所有的线程执行完毕
        fmt.Println(count)
    
    }

    这次当我们多次运行的时候,就能保证我们每次都能看到我们想要的值:200000
    接下来看读写锁

    读写锁

    读写锁主要用到读多写少的场景
    读写锁分为:读锁和写锁

    如果自己设置了一个写锁,那么其他读的线程以及写的线程都拿不到锁,这个时候和互斥锁的功能相同
    如果自己设置了一个读锁,那么其他写的线程是拿不到锁的,但是其他读的线程都是可以拿到这个锁

    我们把上面的例子代码进行更改:

    package main
    
    import (
        "sync"
        "fmt"
    )    
    
    var (
        rwlock sync.RWMutex
        w sync.WaitGroup
        count int
    )
    
    
    func main() {
        w.Add(1)
        go func(){
            for i:=0;i<1000000;i++{
                rwlock.Lock() // 这里定义了一个写锁
                count++
                rwlock.Unlock()
            }
            w.Done()
        }()
    
        for i:=0;i<1000000;i++{
            rwlock.Lock() // 这里定义了一个写锁
            count++
            rwlock.Unlock()
        }
        w.Wait()
        fmt.Println(count)
    }

    通过设置写锁,我们同样可以实现数据的一致性
    下面是一个读锁的使用例子:

    package main
    
    import (
        "sync"
        "fmt"
    )
    
    var (
        rwlock sync.RWMutex
        w sync.WaitGroup
        count int
    )
    
    
    func main() {
        w.Add(1)
        go func(){
            for i:=0;i<1000000;i++{
                rwlock.Lock() // 这里定义了一个写锁
                count++
                rwlock.Unlock()
            }
            w.Done()
        }()
    
        for i:=0;i<16;i++{
            w.Add(1)
            go func(){
                rwlock.RLock() //这里定义了一个读锁
                fmt.Println(count)
                rwlock.RUnlock() //释放读锁
                w.Done()
            }()
        }
        w.Wait()
        fmt.Println(count)
    }

    Go中的原子操作

    原子操作,我们则不需加锁,也能保证数据的一致性
    并且如果只是计算,那么原子操作则是最快的

    实例代码:

    package main
    
    import (
        "sync"
        //"time"
        "sync/atomic"
        "fmt"
    )
    
    var (
        w sync.WaitGroup
        count int32
    )
    
    
    func main() {
        w.Add(1)
        //start := time.Now().UnixNano()
        go func() {
            for i:=0;i<1000000;i++{
                atomic.AddInt32(&count,1)
            }
            w.Done()
        }()
    
        for i:=0;i<1000000;i++{
            atomic.AddInt32(&count,1)
        }
        w.Wait()
        //end := time.Now().UnixNano()
        //fmt.Println((end- start)/1000/1000)
        fmt.Println(count)
    }

    关于互斥锁的补充

    互斥锁需要注意的问题:

    1、不要重复锁定互斥锁

    2、不要忘记解锁互斥锁, 必要时使用defer语句

    3、不要对尚未锁定或者已解锁的互斥锁解锁

    4、不要对在多个函数之间直接传递互斥锁

    对已经锁定的互斥锁进行锁定,会立即阻塞当前的goroutine 这个goroutine所执行的流程会一直停滞在该调用互斥锁的Lock方法的那行代码

    所谓死锁: 当前程序中的主goroutine以及我们启用的那些goroutine 都已经被阻塞,这些goroutine可以被称为用户级的goroutine 这就相当于整个程序已经停滞不前了,并且这个时候go程序会抛出如下的panic:

    fatal error: all goroutines are asleep - deadlock!

    并且go语言运行时系统抛出自行抛出的panic都属于致命性错误,都是无法被恢复的,调用recover函数对他们起不到任何作用

    Go语言中的互斥锁是开箱即用的,也就是我们声明一个sync.Mutex 类型的变量,就可以直接使用它了,需要注意:该类型是一个结构体类型,属于值类型的一种,把它传给一个函数

    将它从函数中返回,把它赋值给其他变量,让它进入某个通道都会导致他的副本的产生。并且原值和副本以及多个副本之间是完全独立的,他们都是不同的互斥锁

    所以不应该将锁通过函数的参数进行传递

    关于读写锁的补充

    1、在写锁已被锁定的情况下再次试图锁定写锁,会阻塞当前的goroutine

    2、在写锁已被锁定的情况下再次试图锁定读锁,也会阻塞当前的goroutine

    3、在读锁已被锁定的情况下试图锁定写锁,同样会阻塞当前的goroutine

    4、在读锁已被锁定的情况下再试图锁定读锁,并不会阻塞当前的goroutine

    对于某个受到读写锁保护的共享资源,多个写操作不能同时进行,写操作和读操作也不能同时进行,但多个读操作却可以同时进行

    对写锁进行解锁,会唤醒“所有因试图锁定读锁,而被阻塞的goroutine”, 并且这个通常会使他们都成功完成对读锁的锁定

    对读锁进行解锁,只会在没有其他读锁锁定的前提下,唤醒“因试图锁定写锁,而被阻塞的goroutine” 并且只会有一个被唤醒的goroutine能够成功完成对写锁的锁定,其他的goroutine

    还要在原处继续等待,至于哪一个goroutine,那么就要看谁等待的事件最长

    解锁读写锁中未被锁定的写锁, 会立即引发panic ,对其中的读锁也是如此,并且同样是不可恢复的

  • 相关阅读:
    Atitit 经济学常见的流派 古典主义与凯恩斯主义
    Atitit 学习方法 体系化学习方法 Excel 科目,分类,专业 三级分类。。 知识点。。 课程就是每一个知识点的详细化。。 比如经济学 类别 专业 xx概论知识点 3、金
    atiitt it学科体系化 体系树与知识点概念大总结.xlsx
    Atitit 减少财政支出普通人如何蹭政府补贴措施 attilax大总结.docx
    Atitit 信用管理概论 attilax学习心得
    Atitit.月度计划日程表 每月流程表v5
    Atitit 企业6大职能 attilax总结
    Atitit 常见每日流程日程日常工作.docx v8 ver ampm imp 签到 am y 天气情况检查 am y 晨会,每天或者隔天 am 每日计划(项目计划,日计划等。 am
    Atitit 财政赤字解决方案
    Atitit 建设自己的财政体系 attilax总结 1.1. 收入理论 2 1.2. 收入分类 2 1.3. 2 1.4. 非货币收入 2 1.5. 2 1.6. 降低期望 2 1.7.
  • 原文地址:https://www.cnblogs.com/zhaof/p/8636384.html
Copyright © 2011-2022 走看看