0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Go语言sync包中的锁都在什么场景下用

马哥Linux运维 来源:JWang的博客 作者:JWang 2021-10-26 09:35 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

Go 语言 sync 包中的锁都在什么场景下用?怎么用?本文对 sync 包内的锁做了梳理。

今天谈一下锁,以及 Go 里面 Sync 包里面自带的各种锁,说到锁这个概念,在日常生活中,锁是为了保护一些东西,比如门锁、密码箱锁,可以理解对资源的保护。在编程里面,锁也是为了保护资源,比如说对文件加锁,同一时间只也许一个用户修改,这种锁一般叫作文件锁。

实际开发中,锁又可分为互斥锁(排它锁)、读写锁、共享锁、自旋锁,甚至还有悲观锁、乐观锁这种说法。在 Mysql 数据库里面锁的应用更多,比如行锁、表锁、间隙锁,有点眼花缭乱。抛开这些概念,在编程领域,锁的本质是为了解决并发情况下对数据资源的访问问题,如果我们不加锁,并发读写一块数据必然会产生问题,如果直接加个互斥锁问题是解决了,但是会严重影响读写性能,所以后面又产生了更复杂的锁机制,在数据安全性和性能之间找到最佳平衡点。

正常来说,只有在并发编程下才会需要锁,比如说多个线程(在 Go 里面则是协程)同时读写一个文件,下面我以一个文件为例,来解释这几种锁的概念:

如果我们使用互斥锁,那么同一时间只能由一线程去操作(读或写),这就是像是咱们去上厕所,一个坑位同一时间只能蹲一个人,这就是厕所门锁的作用。

如果我们使用读写锁,意味着可以同时有多个线程读取这个文件,但是写的时候不能读,并且只能由一个线程去写。这个锁实际上是互斥锁的改进版,很多时候我们之所以给文件加锁是为了避免你在写的过程中有人读到了脏数据。

如果我们使用共享锁,根据我查到资料,这种叫法大多数是源自 MySQL 事务里面的锁概念,它意味着只能读数据,并不能修改数据。

如果我们使用自旋锁,则意味着当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

这些锁的机制在 Go 里面有什么应用呢,下面大家一起看看 Go 标准库里面 sync 包提供的一些非常强大的基于锁的实现。

1. 文件锁

文件锁和 sync 包没关系,这里面只是顺便说一下,举个例子,磁盘上面有一个文件,必须保证同一时间只能由一个人打开,这里的同一时间是指操作系统层面的,并不是指应用层面,文件锁依赖于操作系统实现。

在 C 或 PHP 里面,文件锁会使用一个 flock 的函数去实现,其实 Go 里面也类似:

funcmain(){
varf="/var/logs/app.log"
file,err:=os.OpenFile(f,os.O_RDWR,os.ModeExclusive)
iferr!=nil{
panic(err)
}
deferfile.Close()

//调用系统调用加锁
err=syscall.Flock(int(file.Fd()),syscall.LOCK_EX|syscall.LOCK_NB)
iferr!=nil{
panic(err)
}
defersyscall.Flock(int(file.Fd()),syscall.LOCK_UN)
//读取文件内容
all,err:=ioutil.ReadAll(file)
iferr!=nil{
panic(err)
}

fmt.Printf("%s",all)
time.Sleep(time.Second*10)//模拟耗时操作
}

需要说明一下,Flock 函数第一个参数是文件描述符,第二个参数是锁的类型,分为 LOCK_EX(排它锁)、LOCK_SH(读共享锁)、LOCK_NB(遭遇锁的表现,遇到排它锁的时候默认会被阻塞,NB 即非阻塞,直接返回 Error)、LOCK_UN(解锁)。

如果这时候你打开另外一个终端再次运行这个程序你会发现报错信息如下:

panic:resourcetemporarilyunavailable

文件锁保证了一个文件在操作系统层面的数据读写安全,不过实际应用中并不常见,毕竟大部分时候我们都是使用数据库去做数据存储,极少使用文件。

2.sync.Mutex

下面我所说的这些锁都是应用级别的锁,位于 Go 标准库 sync 包里面,各有各的应用场景。

这是一个标准的互斥锁,平时用的也比较多,用法也非常简单,lock 用于加锁,unlock 用于解锁,配合 defer 使用,完美。

为了更好的展示锁的应用,这个举一个没有实际意义的例子,给一个 int 变量做加法,用 2 个协程并发的去做加法。

variint

funcmain(){
goadd(&i)

time.Sleep(time.Second*3)

println(i)
}

funcadd(i*int){
forj:=0;j< 10000;j++{
*i=*i+1
}
}

我们想要得到的正常结果是 20000,然而实际上并不是,其结果是不固定的,很可能少于 20000,大家多运行几次便可得知。

假设你多加一行 runtime.GOMAXPROCS(1),你会发现结果一直是正确的,这是为什么呢?

用一个比较理论的说法,这是因为产生了数据竞争(data race)问题,在 Go 里面我们可以在 go run 后面加上-race来检测数据竞争,结果会告诉你在哪一行产生的,非常实用。

gorun-racemain.go
==================
WARNING:DATARACE
Readat0x00000056ccb8bygoroutine7:
main.add()
main.go:23+0x43
Previouswriteat0x00000056ccb8bygoroutine6:
main.add()
main.go:23+0x59
Goroutine7(running)createdat:
main.main()
main.go:14+0x76
Goroutine6(running)createdat:
main.main()
main.go:13+0x52
==================
20000
Found1datarace(s)
exitstatus66

解决这个问题,有多种解法,我们当然可以换个写法,比如说用 chan 管道去做加法(chan 底层也用了锁),实际上在 Go 里面更推荐去使用 chan 解决数据同步问题,而不是直接用锁机制。

在上面的这个例子里面我们需要在 add 方法里面写,每次操作之前 lock,然后 unlock:

funcadd(i*int){
forj:=0;j< 10000;j++{
s.Lock()
*i=*i+1
s.Unlock()
}
}

3.sync.RWMutex

读写锁是互斥锁的升级版,它最大的优点就是支持多读,但是读和写、以及写与写之间还是互斥的,所以比较适合读多写少的场景。

它的实现里面有 5 个方式:

func(rw*RWMutex)Lock()
func(rw*RWMutex)RLock()
func(rw*RWMutex)RLocker()Locker
func(rw*RWMutex)RUnlock()
func(rw*RWMutex)Unlock()

其中 Lock() 和 Unlock() 用于申请和释放写锁,RLock() 和 RUnlock() 用于申请和释放读锁,RLocker() 用于返回一个实现了 Lock() 和 Unlock() 方法的 Locker 接口

实话说,平时这个用的真不多,主要是使用起来比较复杂,虽然在读性能上面比Mutex要好一点。

4.sync.Map

这个类型印象中是后来加的,最早很多人使用互斥锁来并发的操作 map,现在也还有人这么写:

typeUserstruct{
mmap[string]string
lsync.Mutex
}

也就是一个 map 配一把锁的写法,可能是这种写法比较多,于是乎官方就在标准库里面实现了一个sync.Map, 是一个自带锁的 map,使用起来方便很多,省心。

varmsync.Map

funcmain(){
m.Store("1",1)
m.Store("2",1)
m.Store("3",1)
m.Store(4,"5")//注意类型

load,ok:=m.Load("1")
ifok{
fmt.Printf("%v
",load)
}

load,ok=m.Load(4)
ifok{
fmt.Printf("%v
",load)
}
}

需要注意的一点是这个 map 的 key 和 value 都是 interface{}类型,所以可以随意放入任何类型的数据,在使用的时候就需要做好断言处理。

5.sync.Once

packagemain

import"sync"

varoncesync.Once

funcmain(){
doOnce()
}

funcdoOnce(){
once.Do(func(){
println("one")
})
}

执行结果只打印了一个 one,所以 sync.Once 的功能就是保证只执行一次,也算是一种锁,通常可以用于只能执行一次的初始化操作,比如说单例模式里面的懒汉模式可以用到。

6.sync.Cond

这个一般称之为条件锁,就是当满足某些条件下才起作用的锁,啥个意思呢?举个例子,当我们执行某个操作需要先获取锁,但是这个锁必须是由某个条件触发的,其中包含三种方式:

等待通知:wait, 阻塞当前线程,直到收到该条件变量发来的通知

单发通知:signal, 让该条件变量向至少一个正在等待它的通知的线程发送通知,表示共享数据的状态已经改变

广播通知:broadcast, 让条件变量给正在等待它的通知的所有线程都发送通知

下面看一个简单的例子:

packagemain
import(
"sync"
"time"
)

varcond=sync.NewCond(&sync.Mutex{})

funcmain(){
fori:=0;i< 10;i++{
gofunc(iint){
cond.L.Lock()
cond.Wait()//等待通知,阻塞当前goroutine
println(i)
cond.L.Unlock()
}(i)
}

//确保所有协程启动完毕
time.Sleep(time.Second*1)

cond.Signal()

//确保结果有时间输出
time.Sleep(time.Second*1)
}

开始我们使用 for 循环启动 10 个协程,每个协程都在等待锁,然后使用 signal 发送一个通知。

如果你多次运行,你会发现打印的结果也是随机从 0 到 9,说明各个协程之间是竞争的,锁是起到作用的。如果把 singal 替换成 broadcast,则会打印所有结果。

讲实话,我暂时也没有发现有哪些应用场景,感觉这个应该适合需要非常精细的协程控制场景,大家先了解一下吧。

7.sync.WaitGroup

这个大多数人都用过,一般用来控制协程执行顺序,大家都知道如果我们直接用 go 启动一个协程,比如下面这个写法:

gofunc(){
println("1")
}()

time.Sleep(time.Second*1)//睡眠1s

如果没有后面的 sleep 操作,协程就得不到执行,因为整个函数结束了,主进程都结束了协程哪有时间执行,所以有时候为了方便可以直接简单粗暴的睡眠几秒,但是实际应用中不可行。这时候就可以使用 waitGroup 解决这个问题,举个例子:

packagemain

import"sync"

varwgsync.WaitGroup

funcmain(){
fori:=0;i< 10;i++{
wg.Add(1)//计数+1
gofunc(){
println("1")
wg.Done()//计数-1,相当于wg.add(-1)
}()
}
wg.Wait()//阻塞带等待所有协程执行完毕
}

8.sync.Pool

这是一个池子,但是却是一个不怎么可靠的池子,sync.Pool 初衷是用来保存和复用临时对象,以减少内存分配,降低 CG 压力。

说它不可靠是指放进 Pool 中的对象,会在说不准什么时候被 GC 回收掉,所以如果事先 Put 进去 100 个对象,下次 Get 的时候发现 Pool 是空也是有可能的。

packagemain

import(
"fmt"
"sync"
)

typeUserstruct{
namestring
}

varpool=sync.Pool{
New:func()interface{}{
returnUser{
name:"defaultname",
}
},
}

funcmain(){
pool.Put(User{name:"name1"})
pool.Put(User{name:"name2"})

fmt.Printf("%v
",pool.Get())//{name1}
fmt.Printf("%v
",pool.Get())//{name2}
fmt.Printf("%v
",pool.Get())//{defaultname}池子已空,会返回New的结果
}

从输出结果可以看到,Pool 就像是一个池子,我们放进去什么东西,但不一定可以取出来(如果中间有 GC 的话就会被清空),如果池子空了,就会使用之前定义的 New 方法返回的结果。

为什么这个池子会放到 sync 包里面呢?那是因为它有一个重要的特性就是协程安全的,所以其底层自然也用到锁机制。

至于其应用场景,知名的 Web 框架 Gin 里面就有用到,在处理用户的每条请求时都会为当前请求创建一个上下文环境 Context,用于存储请求信息及相应信息等。Context 满足长生命周期的特点,且用户请求也是属于并发环境,所以对于线程安全的 Pool 非常适合用来维护 Context 的临时对象池。

编辑:jq
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • Web
    Web
    +关注

    关注

    2

    文章

    1302

    浏览量

    73638
  • 函数
    +关注

    关注

    3

    文章

    4406

    浏览量

    66829
  • go语言
    +关注

    关注

    1

    文章

    159

    浏览量

    9625

原文标题:浅谈 Golang 锁的应用: sync包

文章出处:【微信号:magedu-Linux,微信公众号:马哥Linux运维】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    智能指纹CW32系列有哪些应用场景

    智能指纹CW32系列有哪些应用场景
    发表于 12-05 06:34

    霍尔元件在电子里是如何应用的?几颗?

    霍尔元件在电子主要通过感知磁场变化实现状态检测、自动控制、防撬报警等功能,其应用数量取决于具体设计需求,通常为1-3颗,复杂系统可能更多。以下是对其应用原理和具体数量的详细分析: 霍尔元件在
    的头像 发表于 11-25 15:01 170次阅读

    家用安防场景介绍

    家⽤安防⾏业,针对家⽤监控场景的安防产品业务,主要涉及带有摄像功能的智能家居相关产品, 含摄像头、智能⻔、电⼦猫眼、扫地机器⼈、⼿办玩具、⾛地/桌⾯机器⼈等。
    发表于 10-15 17:10 0次下载

    【HZ-T536开发板免费体验】2 - 交叉编译仓颉编程语言程序到开发板运行

    感谢电子发烧友和合众恒跃提供的试用机会。 引言 上一篇介绍了开箱的结果,接下来测试一华为仓颉编程语言在开发板上运行的效果。最近对华为仓颉编程语言非常感兴趣,所以此次测试重点也是看看仓颉编程
    发表于 07-16 21:27

    大家都在用什么AI软件?有没有好用的免费的AI软件推荐一

    大家都在用什么AI软件?有没有好用的免费的AI软件推荐一?直接发个安装,谢谢。比如deepseek、Chatgpt、豆包、阿里AI、百度AI、腾讯AI,哪个能用、好用?没找到安装
    发表于 07-09 18:30

    从 Java 到 Go:面向对象的巨人与云原生的轻骑兵

    Go 语言在 2009 年被 Google 推出,在创建之初便明确提出了“少即是多(Less is more)”的设计原则,强调“以工程效率为核心,极简规则解决复杂问题”。它与 Java
    的头像 发表于 04-25 11:13 506次阅读

    请问ADS1282SYNC是做什么的,和读写程序有关系吗?

    请问ADS1282SYNC是做什么的,和读写程序有关系吗?我现在只是给接到了上拉3.3V电阻,不知道这是否和我的程序不一直读0有关系?
    发表于 02-08 09:16

    THS1209在TestMode,为什么有SYNC输出?

    信号停止了,仍然有SYNC输出。 2、读数据的过程,/RD信号为由CONV_CLK经过延时产生。/RD低电平的半周期内,数据线上的数据不是稳定的,而是有几次方波样的跳变。 3、在TestMode,Control Regist
    发表于 02-08 06:56

    电路工作原理 自电路与常开电路的区别

    一、自电路工作原理 自电路是电路的一种特殊设计,一旦按开关,电路就能自动保持持续通电状态,直到按其他开关使之断路为止。这种特性使得
    的头像 发表于 01-31 10:07 5404次阅读

    电路的类型和特点

    和应用场景分为以下几种类型: 机械式自电路 : 这种类型的自电路依赖于机械装置来保持电路的状态。例如,一个简单的开关可以设计成在按后自动锁定,直到再次按
    的头像 发表于 01-18 10:03 1631次阅读

    THS8135不需要外部再引入SYNC/BLANK信号,M1/M2/CLK &amp; SYNC/SYNC_T/BLANK信号应该怎样处理?

    我们有如下应用,请教一再这种场景THS8135 的 M1/M2/SYNC/SYNC_T/BLANK pin如何设置(上下拉)。 将CV
    发表于 12-31 07:31

    求助,关于dac8563SYNC_N和LDAC时序问题求解

    我的理解: (1)dac8563上电后默认是 LDAC_N pin 使能, (2) LDAC_N pin 使能情况SYNC_N时序无需考虑 (3)如果要使用同步模式,在LDAC_N pin 使
    发表于 12-20 06:19

    DAC3482在按字宽度输入模式,为啥SYNC信号每16*n个FIFO采样重复一次?

    下图是DAC3482 FIFO的说明。该FIFO的深度是8。在按字宽度输入模式,为啥SYNC信号每16*n个FIFO采样重复一次?按照我的理解SYNC信号是用来重置FIFO写指针
    发表于 12-20 06:04

    DAC38RF84 SYNC信号上电就是高,为什么?

    在调试DAC38RF84时遇到了问题,芯片一上电,即使不配置DAC38RF84寄存器,甚至不供给时钟,两个通道的SYNC就是高。按我的理解,204B协议SYNC信号应该是接收方发送同步请求时
    发表于 12-13 08:38

    影目科技发布全球首款同传翻译眼镜INMO GO2

    近日,搭载紫光展锐W517芯片平台的INMO GO2由影目科技正式推出。作为全球首款专为商务场景设计的智能翻译眼镜,INMO GO2 以“快、准、稳”三大核心优势,突破传统翻译产品局限,为全球商务人士带来高效、自然、稳定的跨
    的头像 发表于 12-11 10:00 1942次阅读