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

    文章

    1231

    浏览量

    68381
  • 函数
    +关注

    关注

    3

    文章

    3846

    浏览量

    61228
  • go语言
    +关注

    关注

    1

    文章

    155

    浏览量

    8913

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

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

收藏 人收藏

    评论

    相关推荐

    Arduino IDE是否有与Xmc2Go兼容的LoRaWAN库?

    我想问一 Arduino IDE 是否有与 Xmc2Go 兼容的 LoRaWAN 库? 我正在尝试使用连接到 Xmc2Go 的 RFM95W Lora 模块通过 LoRaWAN
    发表于 02-27 06:05

    使用go语言实现一个grpc拦截器

    在开发grpc服务时,我们经常会遇到一些通用的需求,比如:日志、链路追踪、鉴权等。这些需求可以通过grpc拦截器来实现。本文使用go语言来实现一个 grpc一元模式(Unary)拦截器,上报链路追踪信息。
    的头像 发表于 12-18 10:13 200次阅读
    使用<b class='flag-5'>go</b><b class='flag-5'>语言</b>实现一个grpc拦截器

    Go编程语言-你应该知道的一切

    Go 编程语言的故事始于 Google,当时三位工程师 Robert Griesemer、Rob Pike 和 Ken Thompson 对 C++ 的复杂性以及缺乏提供高效编译和执行的简单语言感到厌倦。
    的头像 发表于 12-11 17:37 270次阅读

    AD9910的SYNC_CLK没有输出是为什么?

    我想问一,我的是晶体驱动方式,现在没有配置的情况,REFCLK_OUT有25M频率输出,但是SYNC_CLK没有输出,一直为低。这是什么原因?
    发表于 11-27 07:14

    Go语言比Python强多少

    1.都说Go语言性能非常强大,那么到底比Python强多少? 为了比较Go语言和Python语言在单线程性能上的差距,我们可以做一个简单实验
    的头像 发表于 11-02 14:05 254次阅读
    <b class='flag-5'>Go</b><b class='flag-5'>语言</b>比Python强多少

    如何让Python和Go互相调度

    我们曾经研究过如何让Python和Go互相调度,当时发现,将Go语言写的模块打包成动态链接库,就能在Python中进行调度: 优劣互补! Python+Go结合开发的探讨
    的头像 发表于 11-02 11:24 225次阅读
    如何让Python和<b class='flag-5'>Go</b>互相调度

    移动应用高级语言开发——并发探索

    单核设备,任一个时刻只有一个任务能够运行,其运行顺序是不固定的;而在多核场景,同一时间可以多项任务并行。 并发与并行 02►常见的并发模型 2.1►►线程和 线程和
    发表于 08-28 17:08

    基于Go语言的反弹Shell命令生成工具简介

    RevShell 是一个基于Go语言的反弹Shell命令生成工具,旨在帮助安全研究人员和渗透测试人员在需要与目标主机建立反向连接时快速生成相应的Shell代码。
    发表于 08-25 09:45 405次阅读
    基于<b class='flag-5'>Go</b><b class='flag-5'>语言</b>的反弹Shell命令生成工具简介

    Go语言中的整数类型

    Go 语言中,整型可以细分成两个种类十个类型。
    发表于 07-20 15:25 285次阅读

    Go语言常量的声明

    Go 语言中, 常量 表示的是固定的值,常量表达式的值在编译期进行计算,常量的值不可以修改。例如:3 、 Let's go 、 3.14 等等。常量中的数据类型只可以是 布尔型 、 数字型 (整数型、浮点型和复数)
    发表于 07-20 15:24 263次阅读

    Go语言简介和安装方法

    Go 又称 Golang ,是 Google 的 Robert Griesemer,Rob Pike 及 Ken Thompson 开发的一种静态强类型、编译型语言Go 语言语法与
    发表于 07-19 16:33 396次阅读

    浅谈SylixOS 实时操作系统中Go语言应用

    Go 语言是一门编译型语言,继承了编译型语言的高性能、类型安全以及对计算机底层的高可控性等特点,其运行性能可与C/C++媲美。Go
    发表于 06-08 10:41 747次阅读
    浅谈SylixOS 实时操作系统中<b class='flag-5'>Go</b><b class='flag-5'>语言</b>应用

    Go语言运算符主要包括哪些呢?

    Go语言运算符主要包括:算数运算符、关系运算符、逻辑运算符、位运算符、赋值运算符和其他运算符。
    的头像 发表于 05-26 15:54 575次阅读
    <b class='flag-5'>Go</b><b class='flag-5'>语言</b>运算符主要包括哪些呢?

    一个文档把Go语言所有核心知识点撸全了

    Go语言的主要特征、Golang内置类型和函数、lnit函数和main函数、命令、运算符、下划线、变量和常量、基本类型、数组Array、切片Slice、指针、Map、架构体
    的头像 发表于 05-10 10:05 771次阅读
    一个文档把<b class='flag-5'>Go</b><b class='flag-5'>语言</b>所有核心知识点撸全了

    Go语言中的包

    每个 Go 文件都属于且仅属于一个包,一个包可以由许多以 .go 为扩展名的源文件组成,因此文件名和包名一般来说都是不相同的。
    的头像 发表于 04-17 09:22 1086次阅读