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

    文章

    1255

    浏览量

    69339
  • 函数
    +关注

    关注

    3

    文章

    4307

    浏览量

    62432
  • go语言
    +关注

    关注

    1

    文章

    158

    浏览量

    9029

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

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

收藏 人收藏

    评论

    相关推荐

    在学习go语言的过程踩过的坑

    作为一个5年的phper,这两年公司和个人都在顺应技术趋势,新项目慢慢从php转向了go语言,从2021年到现在,笔者手上也先后开发了两个go项目。在学习
    的头像 发表于 11-11 09:22 111次阅读

    go语言如何解决并发问题

    GO快很多。现在工作也还是有一些老项目在使用PHP,但21年之后的新项目基本上就都是GO了。那为什么PHP那么香,还要转战使用GO呢,下
    的头像 发表于 10-23 13:38 111次阅读
    <b class='flag-5'>go</b><b class='flag-5'>语言</b>如何解决并发问题

    三十分钟入门基础Go Java小子版

    前言 Go语言定义 Go(又称 Golang)是 Google 的 Robert Griesemer,Rob Pike 及 Ken Thompson 开发的一种静态、强类型、编译型语言
    的头像 发表于 08-12 14:32 673次阅读
    三十分钟入门基础<b class='flag-5'>Go</b> Java小子版

    存器电路的中间是什么元件

    存器电路概述 定义与功能 存器(Latch)是数字电路的一种基本存储元件,用于存储一个位(1或0)的状态。它能够在特定输入脉冲电平作用改变状态,并保持该状态直到下一个脉冲电平到
    的头像 发表于 07-23 11:29 329次阅读

    自旋和互斥的使用场景是什么

    自旋和互斥是两种常见的同步机制,它们在多线程编程中被广泛使用。在本文中,我们将介绍自旋和互斥的使用场景,以及它们在不同
    的头像 发表于 07-10 10:05 897次阅读

    请问STVP+COSMIC环境go to definition怎么

    STVP+COSMIC环境go to definition怎么? 我现在go to definition在一个宏定义的时候有效果,但是函数什么的没用,是怎么回事呢,是不是工程里面
    发表于 05-11 06:11

    关于go接口类型的表示方法

    go是一个静态性语言,每个变量都有静态的类型,因此每个变量在编译阶段中有明确的变量类型,比如像:int、float32、MyType。
    的头像 发表于 04-28 10:13 334次阅读

    fpga的是什么编程语言 fpga什么语言开发

    fpga的是什么编程语言 FPGA(现场可编程逻辑门阵列)主要使用的编程语言是硬件描述语言(HDL)。在众多的HDL,Verilog H
    的头像 发表于 03-14 17:09 3321次阅读

    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 635次阅读
    使用<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 617次阅读

    使用FPGA实现对AD7768芯片产生的多路AD数据进行采集,是否需要使用START信号和SYNC_OUT和SYNC_IN信号?

    SYNC_OUT和SYNC_IN信号?文档说不使用START的话,可以接上拉电阻拉高,但是没有说SYNC_IN和SYNC_OUT怎么处理
    发表于 12-11 07:24

    Golang接口的作用和应用场景

    Golang(Go)作为一门现代的静态类型编程语言,提供了许多强大的特性,其中之一便是接口(interface)。接口是Golang的一个核心概念,它具有广泛的应用场景,可以帮助开发
    的头像 发表于 12-05 10:44 1095次阅读

    redis分布式的应用场景有哪些

    Redis分布式是一种基于Redis实现的分布式机制,可以在分布式环境确保资源的独占性,避免并发访问时的数据争问题。下面将详细介绍Redis分布式
    的头像 发表于 12-04 11:21 1396次阅读

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

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