• Go语言基础之13--线程安全及互斥锁和读写锁


    一、线程安全介绍

    1.1 现实例子

    A. 多个goroutine同时操作一个资源,这个资源又叫临界区

    B. 现实生活中的十字路口,通过红路灯实现线程安全

    C. 火车上的厕所(进去之后先加锁,在上厕所,不加锁两个人都进去就出问题了,出来后在解锁,别人就可以使用了),通过互斥锁来实现线程安全

    D、在程序中,同一个变量多个goroutine去修改的时候,肯定是不允许同时修改的,同时修改肯定会出问题,所以当一个goroutine在修改之前需要加锁,修改结束在解锁,这样别的goroutine就可以去修改了。

    1.2 实际例子

    x = x +1

    A. 先从内存中取出x的值

    B. CPU进行计算, x+1

    C. 然后把x+1的结果存储在内存中

    解释:

    就是两个goroutine同时去操作x(共享资源),最后的结果x并不是2,由于线程安全的问题,导致最后的结果还是等于1;

    详情也如下图所示:

    下面来看一个实际例子:

    test1和test2函数都是在自增到1000000(对同一个变量count进行修改)

    1)当test1函数和test2函数跑在同一个线程时:

    package main
    
    import (
        "fmt"
    )
    
    var count int
    
    func test1() {
        for i := 0; i < 1000000; i++ {
            count++
        }
    }
    
    func test2() {
        for i := 0; i < 1000000; i++ {
            count++
        }
    }
    
    func main() {
        test1()
        test2()
        fmt.Printf("count=%d
    ", count)
    }

     执行结果如下:

    因为是串行执行,所以最终结果肯定是2000000

    2)当test1函数和test2函数独自起goroutine运行时:

    package main
    
    import (
        "fmt"
        "time"
    )
    
    var count int
    
    func test1() {
        for i := 0; i < 1000000; i++ {
            count++
        }
    }
    
    func test2() {
        for i := 0; i < 1000000; i++ {
            count++
        }
    }
    
    func main() {
        go test1()
        go test2()
    
        time.Sleep(time.Second)
        fmt.Printf("count=%d
    ", count)
    }

     执行结果如下:

    解释:

    可以看到当test1和test2同时运行对count(共享资源)进行修改时,就会出现冲突,最终结果也就不是2000000了

    1.3 如何解决?

    那么如何解决上述线程安全问题呢,就是我们接下来要学习的互斥锁。

    第2章 互斥锁

    2.1 互斥锁介绍

    A. 同时有且只有一个线程进入临界区,其他的线程则在等待锁;

    B. 当互斥锁释放之后,等待锁的线程才可以获取锁进入临界区;

    C. 多个线程同时等待同一个锁,唤醒的策略是随机的;

    2.2 互斥锁使用实例

    package main
    
    import (
        "fmt"
        "sync" //互斥锁需要使用这个包。
    
        "time"
    )
    
    var count int
    var mutex sync.Mutex //定义一个锁的变量(互斥锁的关键字是Mutex,其是一个结构体,传参一定要传地址,否则就不对了)
    func test1() {
        for i := 0; i < 1000000; i++ {
            mutex.Lock() //对共享变量操作之前先加锁
            count++
            mutex.Unlock() //对共享变量操作完毕在解锁,这样就保护了共享的资源
        }
    }
    
    func test2() {
        for i := 0; i < 1000000; i++ {
            mutex.Lock()
            count++
            mutex.Unlock()
        }
    }
    
    func main() {
        go test1()
        go test2()
    
        time.Sleep(time.Second)
        fmt.Printf("count=%d
    ", count)
    }

     执行结果如下:

    解释:

    加锁(互斥锁)之后其实是相当于串行(对共享变量进行操作时)执行了,就算是goroutine也不例外。

    2.3 互斥锁高阶实例

    1)未加互斥锁代码(有问题)

    package main
    
    import (
        "fmt"
        "sync"
    )
    
    var x = 0
    
    func increment(wg *sync.WaitGroup) {
        x = x + 1
        wg.Done()
    }
    func main() {
        var w sync.WaitGroup
        for i := 0; i < 1000; i++ {
            w.Add(1)
            go increment(&w)
        }
        w.Wait()
        fmt.Println("final value of x", x)
    }

     执行结果:

    2)添加互斥锁代码

    package main
    
    import (
        "fmt"
        "sync"
    )
    
    var x = 0
    
    func increment(wg *sync.WaitGroup, m *sync.Mutex) {
        m.Lock()
        x = x + 1
        m.Unlock()
        wg.Done()
    }
    func main() {
        var w sync.WaitGroup
        var m sync.Mutex
        for i := 0; i < 1000; i++ {
            w.Add(1)
            go increment(&w, &m)
        }
        w.Wait()
        fmt.Println("final value of x", x)
    }

     执行结果:

    三、读写锁

    3.1 使用场景

    A. 读多写少的场景;

    B. 分为两种角色,读锁和写锁;

    C. 当一个goroutine获取写锁之后,其他的goroutine获取写锁或读锁都会等待;

    D. 当一个goroutine获取读锁之后,其他的goroutine获取写锁都会等待, 但其他

    goroutine获取读锁时,都会继续获得锁.;

    3.2 读写锁案例演示

    package main
    
    import (
        "sync"
        "time"
    )
    
    var rwlock sync.RWMutex //定义一个锁的变量(读写锁的关键字是RWMutex,其是一个结构体,传参一定要传地址,否则就不对了)
    var wg sync.WaitGroup
    var count int
    
    func writer() { //写的线程
        for i := 0; i < 1000; i++ {
            // 加写锁
            rwlock.Lock() //加锁写锁之后,其他goroutine就不能针对该共享变量加读锁或写锁(读取或写入)了
            count++
            time.Sleep(10 * time.Millisecond) //模拟写操作需要10ms
            // 释放写锁
            rwlock.Unlock()
        }
        wg.Done()
    }
    
    func reader() { //读的线程
        for i := 0; i < 1000; i++ {
            // 加读锁
            rwlock.RLock() //对于读锁来说,其他goroutine依然可以对该共享变量进行读取(读锁)依然可以,但是写入不行,获取写锁需要等待。
            _ = count
            //fmt.Printf("count=%d
    ", count)
            time.Sleep(1 * time.Millisecond) //模拟读操作场景需要1ms
            // 释放读锁
            rwlock.RUnlock()
        }
        wg.Done()
    }
    
    func main() {
        wg.Add(1)
        go writer()
        for i := 0; i < 10; i++ {
            wg.Add(1)
            go reader() //读锁是并发的,这里加了for循环主要是为了模拟只要有1个goroutine能够读取到共享资源,其他的goroutine也可以获取到。
        }
        wg.Wait()
    }

     执行结果:

    3.3 读写锁和互斥锁性能比较

    针对同一个程序,我们通过比较互斥锁和读写锁的耗时来进行直观展示:

    首先计算读写锁性能:

    代码示例如下:

    package main
    
    import (
        "fmt"
        "sync"
        "time"
    )
    
    var rwlock sync.RWMutex //定义一个锁的变量(读写锁的关键字是RWMutex,其是一个结构体,传参一定要传地址,否则就不对了)
    var wg sync.WaitGroup
    var count int
    
    func writer() { //写的线程
        for i := 0; i < 1000; i++ {
            // 加写锁
            rwlock.Lock() //加锁写锁之后,其他goroutine就不能针对该共享变量加读锁或写锁(读取或写入)了
            count++
            time.Sleep(10 * time.Millisecond) //模拟写操作需要10ms
            // 释放写锁
            rwlock.Unlock()
        }
        wg.Done()
    }
    
    func reader() { //读的线程
        for i := 0; i < 1000; i++ {
            // 加读锁
            rwlock.RLock() //对于读锁来说,其他goroutine依然可以对该共享变量进行读取(读锁)依然可以,但是写入不行,获取写锁需要等待。
            _ = count
            //fmt.Printf("count=%d
    ", count)
            time.Sleep(1 * time.Millisecond) //模拟读操作场景需要1ms
            // 释放读锁
            rwlock.RUnlock()
        }
        wg.Done()
    }
    
    func main() {
    
        start := time.Now().UnixNano() //开始时间
        wg.Add(1)
        go writer()
        for i := 0; i < 10; i++ {
            wg.Add(1)
            go reader() //读锁是并发的,这里加了for循环主要是为了模拟只要有1个goroutine能够读取到共享资源,其他的goroutine也可以获取到。
        }
        wg.Wait()
        end := time.Now().UnixNano() //结束时间
        cost := (end - start) / 1000 / 1000 / 1000
        fmt.Printf("cost %d s
    ", cost)
    
    }

     执行结果如下:

    互斥锁性能:

    见如下实例:

    package main
    
    import (
        "fmt"
        "sync"
        "time"
    )
    
    var mlock sync.Mutex //声明互斥锁变量
    var wg sync.WaitGroup
    var count int
    
    func writer_mutex() { //写的线程
        for i := 0; i < 1000; i++ {
            mlock.Lock()
            count++
            time.Sleep(10 * time.Millisecond) //模拟写操作需要10ms
            mlock.Unlock()
        }
        wg.Done()
    }
    
    func reader_mutex() { //读的线程
        for i := 0; i < 1000; i++ {
            mlock.Lock() //对于多个goroutine来说,互斥锁也是只有1个goroutine可以读,并不像读写锁一样,所有goroutine都可以读
            _ = count
            //fmt.Printf("count=%d
    ", count)
            time.Sleep(1 * time.Millisecond) //模拟读操作场景需要1ms
            mlock.Unlock()
        }
        wg.Done()
    }
    
    func main() {
    
        start := time.Now().UnixNano() //开始时间
        wg.Add(1)
        go writer_mutex()
        for i := 0; i < 10; i++ {
            wg.Add(1)
            go reader_mutex()
        }
        wg.Wait()
        end := time.Now().UnixNano() //结束时间
        cost := (end - start) / 1000 / 1000 / 1000
        fmt.Printf("cost %d s
    ", cost)
    
    }

     执行结果如下:

    总结:

    可以看到最终的结果是同一个程序互斥锁比读写锁耗时多了9秒,主要原因是在读的时候,读写锁可以多个读线程去读,而互斥锁依然只能是一个线程去读,1比10的比例,就造成了最终这个结果。

    葵花宝典

    读多写少用读写锁,读写差不多用互斥锁。

  • 相关阅读:
    python-条件判断
    获取网卡名称
    vSphere Client安装
    python远程执行命令
    xorm操作
    httpd服务安装配置
    error: failed to push some refs to 'git@gitee.com:xxxx'
    三种获取数据的方法fetch和ajax和axios
    react组件的生命周期
    react在移动端的自适应布局
  • 原文地址:https://www.cnblogs.com/forever521Lee/p/9444233.html
Copyright © 2020-2023  润新知