脚本之家,脚本语言编程技术及教程分享平台!
分类导航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|shell|

服务器之家 - 脚本之家 - Golang - GO语言并发之好用的sync包详解

GO语言并发之好用的sync包详解

2022-12-30 15:41机智的程序员小熊 Golang

标准库中的sync包在我们的日常开发中用的颇为广泛,那么大家对sync包的用法知道多少呢,这篇文章就大致讲一下sync包和它的使用,感兴趣的可以学习一下

sync.Map 并发安全的Map

反例如下,两个Goroutine分别读写。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func unsafeMap(){
    var wg sync.WaitGroup
    m := make(map[int]int)
    wg.Add(2)
    go func() {
        defer wg.Done()
        for i := 0; i < 10000; i++ {
            m[i] = i
        }
    }()
 
    go func() {
        defer wg.Done()
        for i := 0; i < 10000; i++ {
            fmt.Println(m[i])
        }
    }()
    wg.Wait()
}

执行报错:

0
fatal error: concurrent map read and map write

goroutine 7 [running]:
runtime.throw({0x10a76fa, 0x0})
......

使用并发安全的Map

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func safeMap() {
    var wg sync.WaitGroup
    var m sync.Map
    wg.Add(2)
    go func() {
        defer wg.Done()
        for i := 0; i < 10000; i++ {
            m.Store(i, i)
        }
    }()
 
    go func() {
        defer wg.Done()
        for i := 0; i < 10000; i++ {
            fmt.Println(m.Load(i))
        }
    }()
    wg.Wait()
}
  • 不需要make就能使用
  • 还内置了StoreLoadLoadOrStoreDeleteRange等操作方法,自行体验。

sync.Once 只执行一次

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

init 函数是当所在的 package 首次被加载时执行,若迟迟未被使用,则既浪费了内存,又延长了程序加载时间。

sync.Once 可以在代码的任意位置初始化和调用,因此可以延迟到使用时再执行,并发场景下是线程安全的。

在多数情况下,sync.Once 被用于控制变量的初始化,这个变量的读写满足如下三个条件:

  • 当且仅当第一次访问某个变量时,进行初始化(写);
  • 变量初始化过程中,所有读都被阻塞,直到初始化完成;
  • 变量仅初始化一次,初始化完成后驻留在内存里。
?
1
2
3
4
5
6
7
8
var loadOnce sync.Once
var x int
for i:=0;i<10;i++{
    loadOnce.Do(func() {
        x++
    })
}
fmt.Println(x)

输出

1

sync.Cond 条件变量控制

sync.Cond 基于互斥锁/读写锁,它和互斥锁的区别是什么呢?

互斥锁 sync.Mutex 通常用来保护临界区和共享资源,条件变量 sync.Cond 用来协调想要访问共享资源的 goroutine

也就是在存在共享变量时,可以直接使用sync.Cond来协调共享变量,比如最常见的共享队列,多消费多生产的模式。

我一开始也很疑惑为什么不使用channelselect的模式来做生产者消费者模型(实际上也可以),这一节不是重点就不展开讨论了。

创建实例

?
1
func NewCond(l Locker) *Cond

NewCond 创建 Cond 实例时,需要关联一个锁。

广播唤醒所有

?
1
func (c *Cond) Broadcast()

Broadcast 唤醒所有等待条件变量 cgoroutine,无需锁保护。

唤醒一个协程

?
1
func (c *Cond) Signal()

Signal 只唤醒任意 1 个等待条件变量 cgoroutine,无需锁保护。

等待

?
1
func (c *Cond) Wait()

每个 Cond 实例都会关联一个锁 L(互斥锁 *Mutex,或读写锁 *RWMutex),当修改条件或者调用 Wait 方法时,必须加锁。

举个不恰当的例子,实现一个经典的生产者和消费者模式,但有先决条件:

  • 边生产边消费,可以多生产多消费。
  • 生产后通知消费。
  • 队列为空时,暂停等待。
  • 支持关闭,关闭后等待消费结束。
  • 关闭后依然可以生产,但无法消费了。
?
1
2
3
4
5
var (
    cnt          int
    shuttingDown = false
    cond         = sync.NewCond(&sync.Mutex{})
)
  • cnt 为队列,这里直接用变量代替了,变量就是队列长度。
  • shuttingDown 消费关闭状态。
  • cond 现成的队列控制。

生产者

?
1
2
3
4
5
6
7
func Add(entry int) {
    cond.L.Lock()
    defer cond.L.Unlock()
    cnt += entry
    fmt.Println("生产咯,来消费吧")
    cond.Signal()
}

消费者

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func Get() (int, bool) {
    cond.L.Lock()
    defer cond.L.Unlock()
    for cnt == 0 && !shuttingDown {
        fmt.Println("未关闭但空了,等待生产")
        cond.Wait()
    }
    if cnt == 0 {
        fmt.Println("关闭咯,也消费完咯")
        return 0, true
    }
    cnt--
    return 1, false
}

关闭程序

?
1
2
3
4
5
6
7
func Shutdown() {
    cond.L.Lock()
    defer cond.L.Unlock()
    shuttingDown = true
    fmt.Println("要关闭咯,大家快消费")
    cond.Broadcast()
}

主程序

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var wg sync.WaitGroup
    wg.Add(2)
    time.Sleep(time.Second)
    go func() {
        defer wg.Done()
        for i := 0; i < 10; i++ {
            go Add(1)
            if i%5 == 0 {
                time.Sleep(time.Second)
            }
        }
    }()
    go func() {
        defer wg.Done()
        shuttingDown := false
        for !shuttingDown {
            var cur int
            cur, shuttingDown = Get()
            fmt.Printf("当前消费 %d, 队列剩余 %d \n", cur, cnt)
        }
    }()
    time.Sleep(time.Second * 5)
    Shutdown()
    wg.Wait()
  • 分别创建生产者与消费者。
  • 生产10个,每5个休息1秒。
  • 持续消费。
  • 主程序关闭队列。

输出

生产咯,来消费吧
当前消费 1, 队列剩余 0 
未关闭但空了,等待生产
生产咯,来消费吧
生产咯,来消费吧
当前消费 1, 队列剩余 1 
当前消费 1, 队列剩余 0 
未关闭但空了,等待生产
生产咯,来消费吧
生产咯,来消费吧
生产咯,来消费吧
当前消费 1, 队列剩余 2 
当前消费 1, 队列剩余 1 
当前消费 1, 队列剩余 0 
未关闭但空了,等待生产
生产咯,来消费吧
生产咯,来消费吧
生产咯,来消费吧
生产咯,来消费吧
当前消费 1, 队列剩余 1 
当前消费 1, 队列剩余 2 
当前消费 1, 队列剩余 1 
当前消费 1, 队列剩余 0 
未关闭但空了,等待生产
要关闭咯,大家快消费
关闭咯,也消费完咯
当前消费 0, 队列剩余 0

小结

1.sync.Map 并发安全的Map。

2.sync.Once 只执行一次,适用于配置读取、通道关闭。

3.sync.Cond 控制协调共享资源。

以上就是GO语言并发之好用的sync包详解的详细内容,更多关于GO语言 sync包的资料请关注服务器之家其它相关文章!

原文链接:https://juejin.cn/post/7182059760567451685

延伸 · 阅读

精彩推荐
  • GolangGo:十个与众不同的特性,你知道吗?

    Go:十个与众不同的特性,你知道吗?

    Go 作为一门相对较新的语言,能够脱颖而出,肯定是多方面的原因。本文聊聊它不同于其他语言的 10 个特性。...

    幽鬼8872022-01-05
  • GolangGo Web编程添加服务器错误和访问日志

    Go Web编程添加服务器错误和访问日志

    这篇文章主要为大家介绍了Go Web编程添加服务器错误日志和访问日志的示例解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早...

    KevinYan117922022-06-26
  • Golanggo语言按显示长度截取字符串的方法

    go语言按显示长度截取字符串的方法

    这篇文章主要介绍了go语言按显示长度截取字符串的方法,涉及Go语言操作字符串的技巧,具有一定参考借鉴价值,需要的朋友可以参考下 ...

    脚本之家7932020-04-15
  • GolangGo语言Web编程实现Get和Post请求发送与解析的方法详解

    Go语言Web编程实现Get和Post请求发送与解析的方法详解

    这篇文章主要介绍了Go语言Web编程实现Get和Post请求发送与解析的方法,结合实例形式分析了Go语言客户端、服务器端结合实现web数据get、post发送与接收数据的...

    typ20044182020-05-06
  • GolangGO语言获取系统环境变量的方法

    GO语言获取系统环境变量的方法

    这篇文章主要介绍了GO语言获取系统环境变量的方法,实例分析了Getenv方法操作环境变量的技巧,具有一定参考借鉴价值,需要的朋友可以参考下 ...

    niuniu7402020-04-19
  • Golanggo语言工程结构

    go语言工程结构

    这篇文章主要简单介绍了go语言工程结构,对于我们学习go语言很有帮助,需要的朋友可以参考下 ...

    hebedich2892020-04-13
  • GolangGolang常量iota的使用实例

    Golang常量iota的使用实例

    今天小编就为大家分享一篇关于Golang常量iota的使用实例,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看...

    _John_Tian_3992020-05-22
  • Golanggo mod 使用旧版本 版本号指定方式

    go mod 使用旧版本 版本号指定方式

    这篇文章主要介绍了go mod 使用旧版本 版本号指定方式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    gs801406152021-06-24