GO并发之好用的sync包

简介: GO并发之好用的sync包

本节源码位置 https://github.com/golang-minibear2333/golang/tree/master/4.concurrent/4.9-sync/


sync.Map 并发安全的Map


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

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

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 被用于控制变量的初始化,这个变量的读写满足如下三个条件:


当且仅当第一次访问某个变量时,进行初始化(写);

变量初始化过程中,所有读都被阻塞,直到初始化完成;

变量仅初始化一次,初始化完成后驻留在内存里。

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来协调共享变量,比如最常见的共享队列,多消费多生产的模式。


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


创建实例

func NewCond(l Locker) *Cond

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

广播唤醒所有

func (c *Cond) Broadcast()

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

唤醒一个协程

func (c *Cond) Signal()


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

等待

func (c *Cond) Wait()


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

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


  • 边生产边消费,可以多生产多消费。
  • 生产后通知消费。
  • 队列为空时,暂停等待。
  • 支持关闭,关闭后等待消费结束。
  • 关闭后依然可以生产,但无法消费了。
var (
  cnt          int
  shuttingDown = false
  cond         = sync.NewCond(&sync.Mutex{})
)


  • cnt 为队列,这里直接用变量代替了,变量就是队列长度。
  • shuttingDown 消费关闭状态。
  • cond 现成的队列控制。

生产者

func Add(entry int) {
  cond.L.Lock()
  defer cond.L.Unlock()
  cnt += entry
  fmt.Println("生产咯,来消费吧")
  cond.Signal()
}

消费者

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
}

关闭程序


func Shutdown() {
  cond.L.Lock()
  defer cond.L.Unlock()
  shuttingDown = true
  fmt.Println("要关闭咯,大家快消费")
  cond.Broadcast()
}


主程序

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

小结


  • sync.Map 并发安全的Map。
  • sync.Once 只执行一次,适用于配置读取、通道关闭。
  • sync.Cond 控制协调共享资源。


引用


相关文章
|
2天前
|
Go
配置go语言下载包 - 蓝易云
这个命令会将包下载到你的GOPATH目录下,并自动安装它。
36 1
|
7天前
|
Cloud Native Go 云计算
多范式编程语言Go:并发与静态类型的结合
Go语言是Google于2007年开发的开源编程语言,旨在提高程序开发和部署的效率。它的独特特征在于结合了并发处理与静态类型系统,提供了简洁、高效、并行处理能力的编程体验。本文将探讨Go语言的特点、应用场景以及其在现代软件开发中的优势。
|
7天前
|
安全 Go
Golang深入浅出之-Go语言中的并发安全队列:实现与应用
【5月更文挑战第3天】本文探讨了Go语言中的并发安全队列,它是构建高性能并发系统的基础。文章介绍了两种实现方法:1) 使用`sync.Mutex`保护的简单队列,通过加锁解锁确保数据一致性;2) 使用通道(Channel)实现无锁队列,天生并发安全。同时,文中列举了并发编程中常见的死锁、数据竞争和通道阻塞问题,并给出了避免这些问题的策略,如明确锁边界、使用带缓冲通道、优雅处理关闭以及利用Go标准库。
27 5
|
7天前
|
存储 缓存 安全
Golang深入浅出之-Go语言中的并发安全容器:sync.Map与sync.Pool
Go语言中的`sync.Map`和`sync.Pool`是并发安全的容器。`sync.Map`提供并发安全的键值对存储,适合快速读取和少写入的情况。注意不要直接遍历Map,应使用`Range`方法。`sync.Pool`是对象池,用于缓存可重用对象,减少内存分配。使用时需注意对象生命周期管理和容量控制。在多goroutine环境下,这两个容器能提高性能和稳定性,但需根据场景谨慎使用,避免不当操作导致的问题。
35 4
|
7天前
|
安全 Go 开发者
Golang深入浅出之-Go语言中的CSP模型:深入理解并发哲学
【5月更文挑战第2天】Go语言的并发编程基于CSP模型,强调通过通信共享内存。核心概念是goroutines(轻量级线程)和channels(用于goroutines间安全数据传输)。常见问题包括数据竞争、死锁和goroutine管理。避免策略包括使用同步原语、复用channel和控制并发。示例展示了如何使用channel和`sync.WaitGroup`避免死锁。理解并发原则和正确应用CSP模型是编写高效安全并发程序的关键。
37 4
|
7天前
|
安全 Go 开发者
Golang深入浅出之-Go语言中的CSP模型:深入理解并发哲学
【5月更文挑战第1天】Go语言基于CSP理论,借助goroutines和channels实现独特的并发模型。Goroutine是轻量级线程,通过`go`关键字启动,而channels提供安全的通信机制。文章讨论了数据竞争、死锁和goroutine泄漏等问题及其避免方法,并提供了一个生产者消费者模型的代码示例。理解CSP和妥善处理并发问题对于编写高效、可靠的Go程序至关重要。
27 2
|
7天前
|
设计模式 Go 调度
Golang深入浅出之-Go语言中的并发模式:Pipeline、Worker Pool等
【5月更文挑战第1天】Go语言并发模拟能力强大,Pipeline和Worker Pool是常用设计模式。Pipeline通过多阶段处理实现高效并行,常见问题包括数据竞争和死锁,可借助通道和`select`避免。Worker Pool控制并发数,防止资源消耗,需注意任务分配不均和goroutine泄露,使用缓冲通道和`sync.WaitGroup`解决。理解和实践这些模式是提升Go并发性能的关键。
31 2
|
7天前
|
安全 测试技术 Go
Golang深入浅出之-Go语言单元测试与基准测试:testing包详解
【4月更文挑战第27天】Go语言的`testing`包是单元测试和基准测试的核心,简化了测试流程并鼓励编写高质量测试代码。本文介绍了测试文件命名规范、常用断言方法,以及如何进行基准测试。同时,讨论了测试中常见的问题,如状态干扰、并发同步、依赖外部服务和测试覆盖率低,并提出了相应的避免策略,包括使用`t.Cleanup`、`t.Parallel()`、模拟对象和检查覆盖率。良好的测试实践能提升代码质量和项目稳定性。
18 1
|
7天前
|
安全 Go
Golang深入浅出之-Go语言标准库中的文件读写:io/ioutil包
【4月更文挑战第27天】Go语言的`io/ioutil`包提供简单文件读写,适合小文件操作。本文聚焦`ReadFile`和`WriteFile`函数,讨论错误处理、文件权限、大文件处理和编码问题。避免错误的关键在于检查错误、设置合适权限、采用流式读写及处理编码。遵循这些最佳实践能提升代码稳定性。
24 0
|
7天前
|
监控 安全 Go
【Go语言专栏】Go语言中的并发性能分析与优化
【4月更文挑战第30天】Go语言以其卓越的并发性能和简洁语法著称,通过goroutines和channels实现并发。并发性能分析旨在解决竞态条件、死锁和资源争用等问题,以提升多核环境下的程序效率。使用pprof等工具可检测性能瓶颈,优化策略包括减少锁范围、使用无锁数据结构、控制goroutines数量、应用worker pool和优化channel使用。理解并发模型和合理利用并发原语是编写高效并发代码的关键。