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就能使用
  • 还内置了Store、Load、LoadOrStore、Delete、Range等操作方法,自行体验。


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 唤醒所有等待条件变量 c 的 goroutine,无需锁保护。

唤醒一个协程

func (c *Cond) Signal()


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

等待

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 控制协调共享资源。


引用


相关文章
|
Go 开发者
Go语言包的组织与导入 -《Go语言实战指南》
本章详细介绍了Go语言中的包(Package)概念及其使用方法。包是实现代码模块化、复用性和可维护性的核心单位,内容涵盖包的基本定义、命名规则、组织结构以及导入方式。通过示例说明了如何创建和调用包,并深入讲解了`go.mod`文件对包路径的管理。此外,还提供了多种导入技巧,如别名导入、匿名导入等,帮助开发者优化代码结构与可读性。最后以表格形式总结了关键点,便于快速回顾和应用。
624 61
|
人工智能 安全 算法
Go入门实战:并发模式的使用
本文详细探讨了Go语言的并发模式,包括Goroutine、Channel、Mutex和WaitGroup等核心概念。通过具体代码实例与详细解释,介绍了这些模式的原理及应用。同时分析了未来发展趋势与挑战,如更高效的并发控制、更好的并发安全及性能优化。Go语言凭借其优秀的并发性能,在现代编程中备受青睐。
585 33
|
JSON 中间件 Go
Go语言实战指南 —— Go中的反射机制:reflect 包使用
Go语言中的反射机制通过`reflect`包实现,允许程序在运行时动态检查变量类型、获取或设置值、调用方法等。它适用于初中级开发者深入理解Go的动态能力,帮助构建通用工具、中间件和ORM系统等。
955 63
|
Java 编译器 Go
【Golang】(1)Go的运行流程步骤与包的概念
初次上手Go语言!先来了解它的运行流程吧! 在Go中对包的概念又有怎样不同的见解呢?
576 4
|
存储 Go 开发者
Go 语言中如何处理并发错误
在 Go 语言中,并发编程中的错误处理尤为复杂。本文介绍了几种常见的并发错误处理方法,包括 panic 的作用范围、使用 channel 收集错误与结果,以及使用 errgroup 包统一管理错误和取消任务,帮助开发者编写更健壮的并发程序。
320 4
Go 语言中如何处理并发错误
|
Go 持续交付 开发者
Go语言包与模块(module)的基本使用-《Go语言实战指南》
本章深入讲解Go语言中的包(Package)和模块(Module)概念。包是代码组织的最小单位,每个`.go`文件属于一个包,通过`import`实现复用;主程序包需命名为`main`。模块是Go 1.11引入的依赖管理机制,支持自动版本管理和私有/远程仓库,无需依赖GOPATH。通过实际示例,如自定义包`mathutil`和第三方模块`gin`的引入,展示其使用方法。常用命令包括`go mod init`、`go mod tidy`等,帮助开发者高效管理项目依赖。最后总结,包负责功能划分,模块实现现代化依赖管理,提升团队协作效率。
605 15
|
数据采集 监控 Go
用 Go 实现一个轻量级并发任务调度器(支持限速)
本文介绍了如何用 Go 实现一个轻量级的并发任务调度器,解决日常开发中批量任务处理的需求。调度器支持最大并发数控制、速率限制、失败重试及结果收集等功能。通过示例代码展示了其使用方法,并分析了核心组件设计,包括任务(Task)和调度器(Scheduler)。该工具适用于网络爬虫、批量请求等场景。文章最后总结了 Go 并发模型的优势,并提出了扩展功能的方向,如失败回调、超时控制等,欢迎读者交流改进。
682 25
|
数据采集 Go API
Go语言实战案例:多协程并发下载网页内容
本文是《Go语言100个实战案例 · 网络与并发篇》第6篇,讲解如何使用 Goroutine 和 Channel 实现多协程并发抓取网页内容,提升网络请求效率。通过实战掌握高并发编程技巧,构建爬虫、内容聚合器等工具,涵盖 WaitGroup、超时控制、错误处理等核心知识点。
|
缓存 监控 安全
告别缓存击穿!Go 语言中的防并发神器:singleflight 包深度解析
在高并发场景中,多个请求同时访问同一资源易导致缓存击穿、数据库压力过大。Go 语言提供的 `singleflight` 包可将相同 key 的请求合并,仅执行一次实际操作,其余请求共享结果,有效降低系统负载。本文详解其原理、实现及典型应用场景,并附示例代码,助你掌握高并发优化技巧。
934 0
|
数据采集 消息中间件 编解码
Go语言实战案例:使用 Goroutine 并发打印
本文通过简单案例讲解 Go 语言核心并发模型 Goroutine,涵盖协程启动、输出控制、主程序退出机制,并结合 sync.WaitGroup 实现并发任务同步,帮助理解 Go 并发设计思想与实际应用。

热门文章

最新文章