Golang并发安全和锁

2023-11-18

目录

场景        

互斥锁

        读写互斥锁

                互斥锁

                读写锁

        Sync.Once

        sync.Map

定时器


场景        

        有时候在Go代码中可能会存在多个goroutine同时操作一个资源(临界区),这种情况会发生竞态问题(数据竞态)。类比现实生活中的例子有十字路口被各个方向的的汽车竞争;还有火车上的卫生间被车厢里的人竞争。

例子:

package main
​
import (
    "fmt"
    "sync"
)
​
var (
    x  int64
    sw sync.WaitGroup
)
​
func add() {
    for i := 0; i < 5000; i++ {
        x = x + 1
    }
    sw.Done()
}
func main() {
​
    sw.Add(2)
    go add()
    go add()
​
    sw.Wait()
    fmt.Println(x)
​
}
​
运行结果

8740
10000

        上面的代码中我们开启了两个goroutine去累加变量x的值,这两个goroutine在访问和修改x变量的时候就会存在数据竞争,导致最后的结果与期待的不符。

互斥锁

        互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。 使用互斥锁来修复上面代码的问题:

package main
​
import (
    "fmt"
    "sync"
)
​
var (
    x    int64
    sw   sync.WaitGroup
    lock sync.Mutex
)
​
func add() {
    for i := 0; i < 5000; i++ {
        //加锁
        lock.Lock()
        x = x + 1
        //解锁
        lock.Unlock()
    }
    sw.Done()
}
func main() {
​
    sw.Add(2)
    go add()
    go add()
​
    sw.Wait()
    fmt.Println(x)
​
}
 

        使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其他的goroutine则在等待锁;当互斥锁释放后,等待的goroutine才可以获取锁进入临界区,多个goroutine同时等待一个锁时,唤醒的策略是随机的。

        读写互斥锁

        互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync包中的RWMutex类型。

        读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。

示例:

                互斥锁

package main
​
import (
    "fmt"
    "sync"
    "time"
)
​
var (
    x int64
​
    //计数器
    sw sync.WaitGroup
​
    //互斥锁
    lock sync.Mutex
​
    //读写锁
    rwlock sync.RWMutex
)
​
//读写函数
​
//读函数
func read() {
    defer sw.Done()
    //读锁加锁
    //rwlock.RLock()
    //互斥锁
    lock.Lock()
    time.Sleep(time.Microsecond * 5)
    //rwlock.RUnlock() //读锁解锁
    //解锁
    lock.Unlock()
}
​
//写函数
func write() {
    defer sw.Done()
    //写锁加锁
    //rwlock.Lock()
    lock.Lock()
    //写入5毫秒
    x++
​
    time.Sleep(time.Microsecond * 5)
​
    lock.Unlock()
    //rwlock.Unlock() //写锁解锁
}
func main() {
    //启动时间戳
    start := time.Now()
​
    //写入10次
    for i := 0; i < 10; i++ {
        sw.Add(1)
​
        go write()
    }
    sw.Wait()
    end1 := time.Now()
    fmt.Printf("写入使用时间%v\n", end1.Sub(start))
​
    //读1000次
    for i := 0; i < 1000; i++ {
        sw.Add(1)
​
        go read()
    }
    sw.Wait()
    end := time.Now()
    fmt.Printf("读使用时间%v\n", end.Sub(start))
}
​
运行结果

PS D:\golang\src\dev_ops\day30\lock\demo3> go run .\main.go
写入使用时间149.4821ms
读使用时间15.8658353s
PS D:\golang\src\dev_ops\day30\lock\demo3> go run .\main.go
写入使用时间117.2153ms
读使用时间15.7770074s

                读写锁

package main
​
import (
    "fmt"
    "sync"
    "time"
)
​
var (
    x int64
​
    //计数器
    sw sync.WaitGroup
​
    //互斥锁
    lock sync.Mutex
​
    //读写锁
    rwlock sync.RWMutex
)
​
//读写函数
​
//读函数
func read() {
    defer sw.Done()
    //读锁加锁
    rwlock.RLock()
    //互斥锁
    //lock.Lock()
    time.Sleep(time.Microsecond * 5)
    rwlock.RUnlock() //读锁解锁
    //解锁
    //lock.Unlock()
}
​
//写函数
func write() {
    defer sw.Done()
    //写锁加锁
    rwlock.Lock()
    //lock.Lock()
    //写入5毫秒
    x++
​
    time.Sleep(time.Microsecond * 5)
​
    //lock.Unlock()
    rwlock.Unlock() //写锁解锁
}
func main() {
    //启动时间戳
    start := time.Now()
​
    //写入10次
    for i := 0; i < 10; i++ {
        sw.Add(1)
​
        go write()
    }
    sw.Wait()
    end1 := time.Now()
    fmt.Printf("写入使用时间%v\n", end1.Sub(start))
​
    //读1000次
    for i := 0; i < 1000; i++ {
        sw.Add(1)
​
        go read()
    }
    sw.Wait()
    end := time.Now()
    fmt.Printf("读使用时间%v\n", end.Sub(start))
}
​
运行结果

PS D:\golang\src\dev_ops\day30\lock\demo3> go run .\main.go
写入使用时间137.349ms
读使用时间152.6864ms
PS D:\golang\src\dev_ops\day30\lock\demo3> go run .\main.go
写入使用时间150.1936ms
读使用时间165.9248ms
PS D:\golang\src\dev_ops\day30\lock\demo3> go run .\main.go
写入使用时间101.9337ms
读使用时间117.9023ms

        Sync.Once

        延迟一个开销很大的初始化操作,到真正用到它的时候再执行,例如:定义了一个init初始化函数,程序启动的时候会被自动加载,无论是否用到都会加载,这样程序就会增加程序的启动延时。

        在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等。

Go语言中的sync包中提供了一个针对只执行一次场景的解决方案–sync.Once。

sync.Once只有一个Do方法,其签名如下:

func (o *Once) Do(f func()) {}

注意:如果要执行的函数f需要传递参数就需要搭配闭包来使用。

加载配置文件示例

        延迟一个开销很大的初始化操作到真正用到它的时候再执行是一个很好的实践。因为预先初始化一个变量(比如在init函数中完成初始化)会增加程序的启动耗时,而且有可能实际执行过程中这个变量没有用上,那么这个初始化操作就不是必须要做的。我们来看一个例子:

var icons map[string]image.Image
​
func loadIcons() {
    icons = map[string]image.Image{
        "left":  loadIcon("left.png"),
        "up":    loadIcon("up.png"),
        "right": loadIcon("right.png"),
        "down":  loadIcon("down.png"),
    }
}
​
// Icon 被多个goroutine调用时不是并发安全的
func Icon(name string) image.Image {
    if icons == nil {
        loadIcons()
    }
    return icons[name]
}

        多个goroutine并发调用Icon函数时不是并发安全的,现代的编译器和CPU可能会在保证每个goroutine都满足串行一致的基础上自由地重排访问内存的顺序。loadIcons函数可能会被重排为以下结果:

func loadIcons() {
    icons = make(map[string]image.Image)
    icons["left"] = loadIcon("left.png")
    icons["up"] = loadIcon("up.png")
    icons["right"] = loadIcon("right.png")
    icons["down"] = loadIcon("down.png")
}

        在这种情况下就会出现即使判断了icons不是nil也不意味着变量初始化完成了。考虑到这种情况,我们能想到的办法就是添加互斥锁,保证初始化icons的时候不会被其他的goroutine操作,但是这样做又会引发性能问题。

使用sync.Once改造的示例代码如下:

var icons map[string]image.Image
​
var loadIconsOnce sync.Once
​
func loadIcons() {
    icons = map[string]image.Image{
        "left":  loadIcon("left.png"),
        "up":    loadIcon("up.png"),
        "right": loadIcon("right.png"),
        "down":  loadIcon("down.png"),
    }
}
​
// Icon 是并发安全的
func Icon(name string) image.Image {
    loadIconsOnce.Do(loadIcons)
    return icons[name]
}

        sync.Once其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。

示例

package main
​
import (
    "fmt"
    "sync"
)
​
var only sync.Once
func test(x int) {
    fmt.Println(x)
}
​
//闭包
func close(x int) func()  {
    return func() {
        test(x)
    }
}
func main() {
​
    //函数变量
    t := close(10)
    only.Do(t)
}
运行结果

10

        sync.Map

Go语言中内置的map不是并发安全的。请看下面的示例:

package main
​
import (
    "fmt"
    "strconv"
    "sync"
)
​
var m = make(map[string]int)
​
//设置map
func set(key string, value int) {
    m[key] = value
}
​
//获取map值
func get(key string) int {
    return m[key]
}
func main() {
    sw := sync.WaitGroup{}
    for i := 0; i < 20; i++ {
        sw.Add(1)
        go func (n int) {
            key := strconv.Itoa(n) //整形转字符串
            set(key,n)             //设置map元素
            fmt.Printf("key:  %s,value:%v\n",key,get(key)) //输出map元素
            sw.Done()
        }(i)
        
    }
    sw.Wait()
}

运行结果

PS D:\golang\src\dev_ops\day30\syncMap\test> go run .\main.go
key:  0,value:0
fatal error: key:  19,value:19        
key:  5,value:5
key:  6,value:6
key:  7,value:7
key:  8,value:8
key:  9,value:9
key:  10,value:10
key:  11,value:11
key:  12,value:12
key:  13,value:13
key:  14,value:14
key:  4,value:4
key:  15,value:15
concurrent map writeskey:  16,value:16
key:  17,value:17
​
key:  18,value:18
​
goroutine 7 [running]:
runtime.throw({0x549dcc, 0x0})
        C:/Program Files/Go/src/runtime/panic.go:1198 +0x76 fp=0xc000055ed0 sp=0xc000055ea0 pc=0x4d3116
runtime.mapassign_faststr(0x0, 0x0, {0x54d5cd, 0x1})
        C:/Program Files/Go/src/runtime/map_faststr.go:211 +0x39c fp=0xc000055f38 sp=0xc000055ed0 pc=0x4b067c
main.set(...)
        D:/golang/src/dev_ops/day30/syncMap/test/main.go:13
main.main.func1(0x1)
        D:/golang/src/dev_ops/day30/syncMap/test/main.go:26 +0x6c fp=0xc000055fc8 sp=0xc000055f38 pc=0x52deac
main.main·dwrap·1()
        D:/golang/src/dev_ops/day30/syncMap/test/main.go:29 +0x2d fp=0xc000055fe0 sp=0xc000055fc8 pc=0x52de0d
runtime.goexit()
        C:/Program Files/Go/src/runtime/asm_amd64.s:1581 +0x1 fp=0xc000055fe8 sp=0xc000055fe0 pc=0x4fca81
created by main.main
        D:/golang/src/dev_ops/day30/syncMap/test/main.go:24 +0x38
​
goroutine 1 [semacquire]:
sync.runtime_Semacquire(0x0)
        C:/Program Files/Go/src/runtime/sema.go:56 +0x25
sync.(*WaitGroup).Wait(0x0)
        C:/Program Files/Go/src/sync/waitgroup.go:130 +0x71
main.main()
        D:/golang/src/dev_ops/day30/syncMap/test/main.go:32 +0xea
​
goroutine 8 [runnable]:
internal/poll.runtime_Semacquire(0xe)
        C:/Program Files/Go/src/runtime/sema.go:61 +0x25
internal/poll.(*fdMutex).rwlock(0xc00008e280, 0x54)
        C:/Program Files/Go/src/internal/poll/fd_mutex.go:154 +0xd2
internal/poll.(*FD).writeLock(...)
        C:/Program Files/Go/src/internal/poll/fd_mutex.go:239
internal/poll.(*FD).Write(0xc00008e280, {0xc000014220, 0x10, 0x10})
        C:/Program Files/Go/src/internal/poll/fd_windows.go:598 +0x6c
os.(*File).write(...)
        C:/Program Files/Go/src/os/file_posix.go:49
os.(*File).Write(0xc000006018, {0xc000014220, 0x12, 0xc000057f98})
        C:/Program Files/Go/src/os/file.go:176 +0x65
fmt.Fprintf({0x567fa0, 0xc000006018}, {0x548f59, 0x12}, {0xc000057f98, 0x2, 0x2})
        C:/Program Files/Go/src/fmt/print.go:205 +0x9b
fmt.Printf(...)
        C:/Program Files/Go/src/fmt/print.go:213
main.main.func1(0x2)
        D:/golang/src/dev_ops/day30/syncMap/test/main.go:27 +0x11b
created by main.main
        D:/golang/src/dev_ops/day30/syncMap/test/main.go:24 +0x38
​
goroutine 9 [semacquire]:
internal/poll.runtime_Semacquire(0xe)
        C:/Program Files/Go/src/runtime/sema.go:61 +0x25
internal/poll.(*fdMutex).rwlock(0xc00008e280, 0x54)
        C:/Program Files/Go/src/internal/poll/fd_mutex.go:154 +0xd2
internal/poll.(*FD).writeLock(...)
        C:/Program Files/Go/src/internal/poll/fd_mutex.go:239
internal/poll.(*FD).Write(0xc00008e280, {0xc000014230, 0x10, 0x10})
        C:/Program Files/Go/src/internal/poll/fd_windows.go:598 +0x6c
os.(*File).write(...)
        C:/Program Files/Go/src/os/file_posix.go:49
os.(*File).Write(0xc000006018, {0xc000014230, 0x12, 0xc00009bf98})
        C:/Program Files/Go/src/os/file.go:176 +0x65
fmt.Fprintf({0x567fa0, 0xc000006018}, {0x548f59, 0x12}, {0xc00009bf98, 0x2, 0x2})
        C:/Program Files/Go/src/fmt/print.go:205 +0x9b
fmt.Printf(...)
        C:/Program Files/Go/src/fmt/print.go:213
main.main.func1(0x3)
        D:/golang/src/dev_ops/day30/syncMap/test/main.go:27 +0x11b
created by main.main
        D:/golang/src/dev_ops/day30/syncMap/test/main.go:24 +0x38
exit status 2

        上面的代码开启少量几个goroutine的时候可能没什么问题,当并发多了之后执行上面的代码就会报fatal error: concurrent map writes错误。

像这种场景下就需要为map加锁来保证并发的安全性了,Go语言的sync包中提供了一个开箱即用的并发安全版map–sync.Map。开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。同时sync.Map内置了诸如Store、Load、LoadOrStore、Delete、Range等操作方法。

package main
​
import (
    "fmt"
    "strconv"
    "sync"
)
​
var m = sync.Map{}
​
func main() {
    sw := sync.WaitGroup{}
    for i := 0; i < 20; i++ {
        sw.Add(1)
        go func(n int) {
            key := strconv.Itoa(n) //整形转字符串
            m.Store(key, n)
​
            value, _ := m.Load(key)
            fmt.Printf("key:  %s,value:%v\n", key, value)
            sw.Done()
        }(i)
​
    }
    sw.Wait()
}
​
运行结果

PS D:\golang\src\dev_ops\day30\syncMap\Map> go run .\syncmap.go
key:  0,value:0
key:  4,value:4
key:  1,value:1
key:  2,value:2
key:  3,value:3
key:  11,value:11
key:  5,value:5
key:  6,value:6
key:  7,value:7
key:  8,value:8
key:  9,value:9
key:  10,value:10
key:  15,value:15
key:  12,value:12
key:  13,value:13
key:  14,value:14
key:  17,value:17
key:  16,value:16
key:  18,value:18
key:  19,value:19

定时器

Ticker:时间到了,多次执行

package main
​
import (
    "fmt"
    "time"
)
​
func main() {
    // 1.获取ticker对象
    ticker := time.NewTicker(1 * time.Second)
    i := 0
    // 子协程
    go func() {
        for {
            //<-ticker.C
            i++
            fmt.Println(<-ticker.C)
            if i == 5 {
                //停止
                ticker.Stop()
            }
        }
    }()
    for {
    }
}
运行结果

PS D:\golang\src\dev_ops\day30\time\demo1> go run .\main.go
2022-04-27 21:36:31.8552045 +0800 CST m=+1.005777801
2022-04-27 21:36:32.8665091 +0800 CST m=+2.017082401
2022-04-27 21:36:33.8661172 +0800 CST m=+3.016690501
2022-04-27 21:36:34.8629154 +0800 CST m=+4.013488701
2022-04-27 21:36:35.8553368 +0800 CST m=+5.005910101
exit status 0xc000013a

示例二

package main
​
import (
    "fmt"
    "time"
)
​
func tickDemo() {
    //定义计时器
    ticker := time.Tick(time.Second*2) //两秒执行一次
​
    for i := range ticker {
        
        fmt.Println(i)
    }
}
func main() {
​
    tickDemo()
}
运行结果

PS D:\golang\src\dev_ops\day30\time\demo2> go run .\ticker.go
2022-04-27 21:39:15.2020295 +0800 CST m=+2.010088101
2022-04-27 21:39:17.2070939 +0800 CST m=+4.015152501
2022-04-27 21:39:19.2033854 +0800 CST m=+6.011444001
2022-04-27 21:39:21.1949244 +0800 CST m=+8.002983001
2022-04-27 21:39:23.2007962 +0800 CST m=+10.008854801
2022-04-27 21:39:25.2087759 +0800 CST m=+12.016834501
2022-04-27 21:39:27.2055817 +0800 CST m=+14.013640301
exit status 0xc000013a

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Golang并发安全和锁 的相关文章

随机推荐

  • 【点云格式互转】ply转bin、任意点云格式转ply

    本文为博主原创文章 未经博主允许不得转载 本文为专栏 python三维点云从基础到深度学习 系列文章 地址为 https blog csdn net suiyingy article details 124017716 3D点云存储方式的种
  • 08_STM32的存储结构

    STM32的存储器结构 存储器是单片机结构的重要组成部分 存储器是用来存储编译好的程序代码和数据的 有了存储器单片机系统才具有记忆功能 按照存储介质的特性 可以分 易失性存储器 和 非易失性存储器 两类 易失性存储器断电后 里面存储的内容会
  • 802.11K/V/R协议介绍

    802 11K V R协议介绍 1 传统无线漫游介绍 1 1 什么是无线漫游 一台无线终端备 STA 通过连接Wi Fi获取上网体验 Wi Fi名称 又称为SSID 是由无线接入网 AP 提供的 因为一台AP设备的覆盖范围有限 所以无线网络
  • 解决同时多个请求的处理问题,和定时任务

    应用场景 发布系统 让他晚上2点执行发布的一串任务 定时任务 更新访问量 晚上2点定时将数据从redis更新到mysql中去 商城类的抢购工作 大批量的用户涌入 承载不了一次性处理这么多的活儿 用这个方式也可以 目录结构 1 让woker运
  • maven项目debug查看依赖包源代码办法

    默认的maven工程 好像很难加载依赖的源代码 办法如下 maven调试时 无法进入源码 办法一 在debug配置里面 找到source 把带source的jar包 放进去 添加的时候 选add 再选external archive 这里要
  • jQuery——前端技术栈

    1 jQuery说明 jQuery 是一个 JavaScript 函数库 是一个轻量级的 写的少 做的多 的 JavaScript 库 jQuery 库包含以下功能 HTML 元素选取 HTML 元素操作 CSS 操作 HTML 事件函数
  • gin框架07--PureJSON

    gin框架07 PureJSON 介绍 案例 说明 介绍 通常 JSON 使用 unicode 替换特殊 HTML 字符 例如 lt 变为 u003c 如果要按字面对这些字符进行编码 则可以使用 PureJSON Go 1 6 及更低版本无
  • DataFrame入门丨Pandas数据分析基础(4)

    个人主页 互联网阿星 格言 选择有时候会大于努力 但你不努力就没得选 作者简介 大家好我是互联网阿星 和我一起合理使用Python 努力做时间的主人 如果觉得博主的文章还不错的话 请点赞 收藏 留言 支持一下博主哦 行业资料 PPT模板 简
  • Qt 6.x中Qt Quick简介及示例

    Qt Quick首次在Qt 4 7和Qt Creator 2 1中引入 是一种高级UI技术 Qt Quick模块是用于编写QML Qt Meta Object Language Qt元对象语言 应用程序的标准库 Qt QML模块提供了QML
  • Linux获取本机的IP的几种方式

    共计五条命令 使用的方式都不同 但原理相同充分利用LInux管道技术 root vagrant centos65 ifconfig eth0 grep Bcast cut d f2 cut d f1 192 168 191 2 root v
  • 西门子PLC的编程工具是什么?

    西门子PLC编程工具主要有下面这几个 1 STEP 7 Simatic Manager STEP 7或者Simatic Manager是西门子PLC编程最常用的软件开发环境 它是一款强大的集成开发环境 可以用来编写 调试PLC编程代码 还可
  • 《基础篇第4章:vue2基础》:使用vue脚手架创建项目

    文章目录 4 1初始化脚手架 4 1 1说明 4 1 2具体步骤 4 1 3模板项目的结构 4 1 4 创建第一个vue项目分析index html组成部分 4 1 5render函数 4 1 6修改默认配置 本人其他相关文章链接 4 1初
  • ubuntu wget ERROR 403: Forbidden 错误

    解决办法 临时变更wget的UA wget U 浏览器的useragent 下载地址 永久变更Wget 未测试 修改配置文件 etc wgetrc 添加下面这一行 header User Agent 浏览器的useragent 上面这个配置
  • 【vue】vue history和hash用法和区别

    vue中模式选择 router gt index js const router new VueRouter mode history base process env BASE URL routes hash和history区别 1 ha
  • 彷徨

    1 什么是hbase HBASE是一个高可靠性 高性能 面向列 可伸缩的分布式存储系统 利用HBASE技术可在廉价PC Server上搭建起大规模结构化存储集群 HBASE的目标是存储并处理大型的数据 更具体来说是仅需使用普通的硬件配置 就
  • 相传韩信才智过人,从不直接清点自己军队的人数,只要让士兵先后以三人一排、五人一排、七人一排地变换队形,而他每次只掠一眼队伍的排尾就知道总人数了。

    题目描述 相传韩信才智过人 从不直接清点自己军队的人数 只要让士兵先后以三人一排 五人一排 七人一排地变换队形 而他每次只掠一眼队伍的排尾就知道总人数了 输入3个非负整数a b c 表示每种队形排尾的人数 a lt 3 b lt 5 c l
  • 贝叶斯优化优化参数,以Kmeans为例

    文章目录 步骤一 构造黑盒目标函数 步骤二 确定取值空间 步骤三 构造贝叶斯优化器 全部代码 贝叶斯优化有几个步骤 构造黑盒目标函数 确定取值空间 构造贝叶斯优化器 下面以kmeans为例 步骤一 构造黑盒目标函数 这个函数主要是运行需要的
  • Git笔记

    一 安装Git Win10 直接到Git官网下载安装程序 按照默认选项安装即可 安装完成后 在开始菜单里找到Git gt Git Bash 打开弹出类似命令行的窗口 则说明Git安装成功 安装完成后还需要对Git进一步设置 在命令行输入 g
  • ubuntu18.04 git clone:Failed to connect to github.com port 443: Connection refused

    git clone问题记录 一 Failed to connect to github com port 443 Connection refused 1 问题 2 解决方法 3 参考博文 二 gnutls handshake failed
  • Golang并发安全和锁

    目录 场景 互斥锁 读写互斥锁 互斥锁 读写锁 Sync Once sync Map 定时器 场景 有时候在Go代码中可能会存在多个goroutine同时操作一个资源 临界区 这种情况会发生竞态问题 数据竞态 类比现实生活中的例子有十字路口