go语言之go语言锁机制(代码片段)

heych heych     2023-04-08     222

关键词:

Go 语言锁机制

Go 语言互斥锁

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

互斥锁

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

func (m *Mutex) Lock()

Lock方法锁住m,如果m已经加锁,则阻塞直到m解锁。

func (m *Mutex) Unlock()

Unlock方法解锁m,如果m未加锁会导致运行时错误。锁和线程无关,可以由不同的线程加锁和解锁。

互斥锁应用

只要有两个goroutine并发访问同一变量,且至少其中的一个是写操作的时候就会发生数据竞争。数据竞争会在两个以上的goroutine并发访问相同的变量且至少其中一个为写操作时发生。

允许多个goroutine访问变量,但是同一时间只允许一个goroutine访问。我们可以用sync包中的Mutex来实现,保证共享变量不会被并发访问。

实例如下:

package main

import (
    "fmt"
    "sync"
)

var (
    m     = make(map[int]int)
    Mlock = new(sync.Mutex)
)

func main() 
    for i := 0; i < 1000; i++ 
        go func(i int) 
            // Mlock.Lock()
            m[i] = i * i
            // Mlock.Unlock()
        (i)
    

    // Mlock.Lock()
    for k, v := range m 
        fmt.Printf("%d * %d = %d
", k, k, v)
    
    // Mlock.Unlock()

运行错误:

fatal error: concurrent map writes

将上述代码中的注释打开则程序正常运行。

当Unlock()在Lock()之前使用时便会报错,实例如下:

package main

import (
    "fmt"
    "sync"
)

func main() 
    var MLock *sync.Mutex
    MLock = new(sync.Mutex)
    MLock.Unlock()
    fmt.Println("hello Mutex")
    MLock.Lock()

运行错误:

panic: sync: unlock of unlocked mutex

当在解锁之前再次进行加锁,便会死锁状态,实例如下:

package main

import (
    "fmt"
    "sync"
)

func main() 
    var MLock *sync.Mutex
    MLock = new(sync.Mutex)
    MLock.Lock()
    fmt.Println("hello Mutex")
    MLock.Lock()

运行错误:

fatal error: all goroutines are asleep - deadlock!

互斥锁只能锁定一次,当在解锁之前再次进行加锁,便会死锁状态,如果在加锁前解锁,便会报错“panic: sync: unlock of unlocked mutex”。

Go 语言读写锁

RWMutex是一个读写锁,该锁可以加多个读锁或者一个写锁,其经常用于读次数远远多于写次数的场景。

func (rw *RWMutex) Lock()

Lock方法将rw锁定为写入状态,禁止其他线程读取或者写入。如果在添加写锁之前已经有其他的读锁和写锁,则lock就会阻塞直到该锁可用,为确保该锁最终可用,已阻塞的 Lock 调用会从获得的锁中排除新的读取器,即写锁权限高于读锁,有写锁时优先进行写锁定。

func (rw *RWMutex) Unlock()

Unlock方法解除rw的写入锁状态,如果m未加写入锁会导致运行时错误。

func (rw *RWMutex) RLock()

RLock方法将rw锁定为读取状态,禁止其他线程写入,但不禁止读取。

func (rw *RWMutex) RUnlock()

Runlock方法解除rw的读取锁状态,如果m未加读取锁会导致运行时错误。

读写锁的写锁只能锁定一次,解锁前不能多次锁定,读锁可以多次,但读解锁次数最多只能比读锁次数多一次,一般情况下我们不建议读解锁次数多余读锁

读写锁应用

读多写少的情况,用读写锁, 协程同时在操作读。

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

var rwLock sync.RWMutex

func main() 
    m := make(map[int]int, 5)
    m[1] = 10
    m[2] = 10
    m[3] = 10
    m[4] = 10
    m[5] = 10
    for i := 0; i < 2; i++ 
        go func() 
            rwLock.Lock()
            m[5] = rand.Intn(100)
            rwLock.Unlock()
        ()
    
    for i := 0; i < 10; i++ 
        go func() 
            rwLock.RLock()
            fmt.Println(m)
            rwLock.RUnlock()
        ()
    
    time.Sleep(time.Second * 2)

当RUnlock()在RLock()之前使用时便会报错,实例如下:

package main

import (
    "fmt"
    "sync"
)

func main() 
    RWLock := new(sync.RWMutex)
    RWLock.RUnlock()
    fmt.Println("hello RWMutex")
    RWLock.RLock()

运行错误:

fatal error: sync: RUnlock of unlocked RWMutex

Go 语言锁性能比较

性能比较

一、测试互斥锁性能,20秒程序执行次数。

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "sync/atomic"
    "time"
)

var MLock sync.Mutex

func main() 
    m := make(map[int]int, 5)
    var count int32
    m[1] = 10
    m[2] = 10
    m[3] = 10
    m[4] = 10
    m[5] = 10
    for i := 0; i < 2; i++ 
        go func() 
            MLock.Lock()
            m[5] = rand.Intn(100)
            time.Sleep(10 * time.Microsecond)
            MLock.Unlock()

        ()
    
    for i := 0; i < 100; i++ 
        go func() 
            for 
                MLock.Lock()
                time.Sleep(time.Millisecond)
                _ = m[5]
                MLock.Unlock()
                atomic.AddInt32(&count, 1)
            
        ()
    
    time.Sleep(time.Second * 20)
    fmt.Println(atomic.LoadInt32(&count))

二、测试读写锁性能,20秒程序执行次数。

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "sync/atomic"
    "time"
)

var rwLock sync.RWMutex

func main() 
    m := make(map[int]int, 5)
    var count int32
    m[1] = 10
    m[2] = 10
    m[3] = 10
    m[4] = 10
    m[5] = 10
    for i := 0; i < 2; i++ 
        go func() 
            rwLock.Lock()
            m[5] = rand.Intn(100)
            time.Sleep(10 * time.Microsecond)
            rwLock.Unlock()
        ()
    
    for i := 0; i < 100; i++ 
        go func() 
            for 
                rwLock.RLock()
                time.Sleep(time.Millisecond)
                _ = m[5]
                rwLock.RUnlock()
                atomic.AddInt32(&count, 1)
            
        ()
    
    time.Sleep(time.Second * 20)
    fmt.Println(atomic.LoadInt32(&count))

Go 语言并发安全的map(sync.map)

sync.Map

在Go 1.6之前,内置的map类型是部分goroutine安全的,并发的读没有问题,并发的写可能有问题。自go 1.6之后,并发地读写map会报错,这在一些知名的开源库中都存在这个问题,所以go 1.9之前的解决方案是额外绑定一个锁,封装成一个新的struct或者单独使用锁都可以。

在Go1.9之前,go自带的map不是并发安全的,也就是说,我们需要自己再封装一层,给map加上把读写锁,比如像下面这样:

type MapWithLock struct 
    sync.RWMutex
    M map[string]Kline

用MapWithLock的读写锁去控制map的并发安全。

但是到了Go1.9发布,它有了一个新的特性,那就是sync.map,它是原生支持并发安全的map,不过它的用法和以前我们熟悉的map完全不一样,主要还是因为sync.map封装了更为复杂的数据结构,以实现比之前加锁map更优秀的性能。

sync.map就是1.9版本带的线程安全map,主要有如下几种方法:

func (m Map) Load(key interface) (value interface, ok bool)

通过提供一个键key,查找对应的值value,如果不存在,则返回nil。ok的结果表示是否在map中找到值

func (m Map) Store(key, value interface)

这个相当于是写map(更新或新增),第一个参数是key,第二个参数是value

func (m Map) LoadOrStore(key, value interface) (actual interface, loaded bool)

通过提供一个键key,查找对应的值value,如果存在返回键的现有值,否则存储并返回给定的值,如果是读取则返回true,如果是存储返回false

func (m Map) Delete(key interface)

通过提供一个键key,删除键对应的值

func (m Map) Range(f func(key, value interface) bool)

循环读取map中的值。

因为for … range map是内置的语言特性,所以没有办法使用for range遍历sync.Map, 但是可以使用它的Range方法,通过回调的方式遍历。

sync.Map应用

package main

import (
    "fmt"
    "sync"
)

type userInfo struct 
    Name string
    Age  int


// 声明 sync.Map
var m sync.Map

func main() 
    // 写入
    m.Store(1, "one")
    m.Store("oldboy", "Go")

    // 查找
    val, ok := m.Load(1)
    fmt.Println("Load : ", val, ok) // one true

    // 查找 or 写入
    val, ok = m.LoadOrStore("oldboy", "Golang")
    fmt.Println("LoadOrStore : ", val, ok) // Go true
    val, ok = m.LoadOrStore("2", "two")
    fmt.Println("LoadOrStore : ", val, ok) // two false

    // range
    m.Range(func(k, v interface) bool 
        fmt.Println(k, v)
        return true
    )

    // 删除
    m.Delete(1)
    m.Delete(2)

    m.Range(func(k, v interface) bool 
        fmt.Println(k, v)
        return true
    )


go语言基础之并发(代码片段)

并发是编程里面一个非常重要的概念,Go语言在语言层面天生支持并发,这也是Go语言流行的一个很重要的原因。Go语言中的并发编程并发与并行并发:同一时间段内执行多个任务并行:同一时刻执行多个任务Go语言的并发通过goro... 查看详情

go语言之通道(代码片段)

1packagemain23import(4"fmt"5)67funcmain()8//channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。9//声明通道类型10varachanint//声明一个int类型的通道,声明之后需要对它进行初始化11fmt.Println(a)12ch:=make(chanint,10)//进行初始化有... 查看详情

go语言基础之并发(代码片段)

Go语言中的并发编程——并发是编程里面一个非常重要的概念,Go语言在语言层面天生支持并发,这也是Go语言流行的一个很重要的原因。并发与并行并发:同一时间段内执行多个任务(你在用微信和两个女朋友聊天)。并... 查看详情

19.go语言基础之并发(代码片段)

...执行多个任务(windows中360在杀毒,同时你也在写代码)Go语言的并发通过goroutine实现。goroutine类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个goroutine并发工作。goroutine是由Go语言的运行时(runtime)调度完成,而... 查看详情

go语言基础之包(代码片段)

Go语言基础之包在工程化的Go语言开发项目中,Go语言的源码复用是建立在包(package)基础之上的。本文介绍了Go语言中如何定义包、如何导出包的内容及如何导入其他包。Go语言的包(package)包介绍包(package)是多个Go源码的集... 查看详情

go语言之go语言反射(代码片段)

GO语言反射反射是指在程序运行期对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。支持反射的语言可以在程序编译... 查看详情

go语言系列之数组和切片(代码片段)

《Go语言系列文章》Go语言系列(一)之Go的安装和使用Go语言系列(二)之基础语法总结1.数组数组用于存储若干个相同类型的变量的集合。数组中每个变量称为数组的元素,每个元素都有一个数字编号——数组下标,该下标从0开始,... 查看详情

go语言之环境搭建和基本命令(代码片段)

目录go语言基础下载go编译器go目录简介gopath简介环境变量配置GOPATHPATHgo语言项目结构IDE下载与配置安装golandgoland里添加goroot和gopath编写第一个GO程序编译go文件在项目路径下编译在其他路径编译go相关命令跨平台编译go语言基础下... 查看详情

go语言调度器源代码情景分析之六:go汇编语言(代码片段)

go语言runtime(包括调度器)源代码中有部分代码是用汇编语言编写的,不过这些汇编代码并非针对特定体系结构的汇编代码,而是go语言引入的一种伪汇编,它同样也需要经过汇编器转换成机器指令才能被CPU执行。需要注意的是... 查看详情

go语言基础之反射(代码片段)

本文介绍了Go语言反射的意义和基本使用。变量的内在机制Go语言中的变量是分为两部分的:类型信息:预先定义好的元信息。值信息:程序运行过程中可动态变化的。反射介绍反射是指在程序运行期对程序本身进行访问和修改的... 查看详情

go语言sync.cond(代码片段)

sync.Cond是一个事件通知,类似于java中的conditional或者wait/notify机制。它有一个重要的作用是,协程之间通过锁进行协调的时候,其中一个协程等待的时候,可以释放锁和资源,并且能够及时完成准备工作。比如经典的生产者消费... 查看详情

go语言系列之反射(代码片段)

变量的内在机制Go语言中的变量是分为两部分的:类型信息:预先定义好的元信息。值信息:程序运行过程中可动态变化的。反射介绍反射是指在程序运行期对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地... 查看详情

go语言自学系列|golang并发编程之mutex互斥锁实现同步(代码片段)

视频来源:B站《golang入门到项目实战[2021最新Go语言教程,没有废话,纯干货!持续更新中...]》一边学习一边整理老师的课程内容及试验笔记,并与大家分享,请移步至知乎网站,谢谢支持!附上... 查看详情

go语言系列之并发编程(代码片段)

Go语言中的并发编程并发与并行并发:同一时间段内执行多个任务(你在用微信和两个女朋友聊天)。并行:同一时刻执行多个任务(你和你朋友都在用微信和女朋友聊天)。Go语言的并发通过goroutine实现。goroutine类似于线程,... 查看详情

go并发编程:锁selectcontext定时器(代码片段)

...tertime.TickerSync(锁)在前面讲channel的时候,我们说到在Go语言并发编程中,倡导使用通信共享内存,不要使用共享内存通信,即goroutine之间尽量通过channel来协作。而在其他的传统语言中,都是通过共享内存加上锁机制来保证并... 查看详情

go语言之反射(代码片段)

一:反射1反射是指在程序运行期对程序本身进行访问和修改的能力2go程序在运行期使用reflect包访问程序的反射信息1查看类型、字段和方法packagemainimport("fmt""reflect")//结构体typeUserstructIdintNamestringAgeint//绑定方法func(uUser)Hello()fmt.Pr... 查看详情

go语言之consul(代码片段)

安装version:"3.1"services:consul:image:consulrestart:alwayscontainer_name:consulports:-8500:8500-8300:8300-8301:8301-8302:8302-8600:8600/udpvolumes:-./data:/consul/data-./data:/consul/configcommand:agen 查看详情

go语言系列之性能调优(代码片段)

...程序的画像,画像就是应用程序使用CPU和内存的情况。Go语言是一个对性能特别看重的语言,因此语言中自带了profiling的库,这篇文章就要讲解怎么在golang中做profiling。Go性能优化Go语言项目中的性能优化主要有以下几个方面:CPU... 查看详情