Go 语言中 sync 包的近距离观察

news2024/11/24 18:53:43

img

让我们来看看负责提供同步原语的 Go 包:sync

sync.Mutex

sync.Mutex 可能是 sync 包中被广泛使用的原语。它允许对共享资源进行互斥操作(即不允许同时访问):

mutex := &sync.Mutex{}

mutex.Lock()
// Update shared variable (e.g. slice, pointer on a structure, etc.)
mutex.Unlock()

必须指出的是 sync.Mutex 无法被复制(就像 sync 包中的所有其他原语一样)。如果一个结构体有一个 sync 字段,必须通过指针进行传递

sync.RWMutex

sync.RWMutex 是一个读写锁。它提供了与我们刚刚看到的 Lock()Unlock() 相同的方法(因为这两个结构都实现了 sync.Locker 接口)。然而,它还允许使用 RLock()RUnlock() 方法进行并发读取:

mutex := &sync.RWMutex{}

mutex.Lock()
// Update shared variable
mutex.Unlock()

mutex.RLock()
// Read shared variable
mutex.RUnlock()

一个 sync.RWMutex 允许至少一个读取者正好一个写入者,而一个 sync.Mutex 则允许正好一个读取者或写入者。

让我们运行一个快速的基准测试来比较这些方法:

func BenchmarkMutexLock(b *testing.B) {
	m := sync.Mutex{}
	for i := 0; i < b.N; i++ {
		m.Lock()
		m.Unlock()
	}
}
func BenchmarkRWMutexLock(b *testing.B) {
	m := sync.RWMutex{}
	for i := 0; i < b.N; i++ {
		m.Lock()
		m.Unlock()
	}
}

func BenchmarkRWMutexRLock(b *testing.B) {
	m := sync.RWMutex{}
	for i := 0; i < b.N; i++ {
		m.RLock()
		m.RUnlock()
	}
}
BenchmarkMutexLock-4       83497579         17.7 ns/op
BenchmarkRWMutexLock-4     35286374         44.3 ns/op
BenchmarkRWMutexRLock-4    89403342         15.3 ns/op

正如我们注意到的那样,读取锁定/解锁 sync.RWMutex 比锁定/解锁 sync.Mutex 更快。另一方面,调用 Lock()/Unlock()sync.RWMutex 上是最慢的操作。

总的来说,当我们有频繁的读取和不经常的写入时,应该使用 sync.RWMutex

sync.WaitGroup

sync.WaitGroup 也经常被使用。它是一个 goroutine 等待一组 goroutine 完成的惯用方式。

sync.WaitGroup 拥有一个内部计数器。如果这个计数器等于 0,Wait() 方法会立即返回。否则,它会被阻塞,直到计数器变为 0。

要增加计数器,我们可以使用 Add(int) 方法。要减少计数器,可以使用 Done()(将计数器减 1)或者使用带有负值的相同的 Add(int) 方法。

在以下示例中,我们将启动八个 goroutine 并等待它们完成:

wg := &sync.WaitGroup{}

for i := 0; i < 8; i++ {
  wg.Add(1)
  go func() {
    // Do something
    wg.Done()
  }()
}

wg.Wait()
// Continue execution

每次我们创建一个 goroutine 时,都会使用 wg.Add(1) 来增加 wg 的内部计数器。我们也可以在 for 循环外部调用 wg.Add(8)

与此同时,每当一个 goroutine 完成时,它会使用 wg.Done() 来减少 wg 的内部计数器。

一旦执行了八个 wg.Done() 语句,主 goroutine 就会继续执行。

sync.Map

sync.Map 是 Go 中的一个并发版本的 map,我们可以:

  • 使用 Store(interface{}, interface{}) 添加元素
  • 使用 Load(interface) interface{} 检索元素
  • 使用 Delete(interface{}) 删除元素
  • 使用 LoadOrStore(interface{}, interface{}) (interface, bool) 检索或添加元素(如果之前不存在)。返回的 bool 值为 true 表示在操作前键存在于 map 中。
  • 使用 Range 在元素上进行迭代
m := &sync.Map{}

// Put elements
m.Store(1, "one")
m.Store(2, "two")

// Get element 1
value, contains := m.Load(1)
if contains {
  fmt.Printf("%s\n", value.(string))
}

// Returns the existing value if present, otherwise stores it
value, loaded := m.LoadOrStore(3, "three")
if !loaded {
  fmt.Printf("%s\n", value.(string))
}

// Delete element 3
m.Delete(3)

// Iterate over all the elements
m.Range(func(key, value interface{}) bool {
  fmt.Printf("%d: %s\n", key.(int), value.(string))
  return true
})

Go 在线测试: https://play.golang.org/p/BO8IDVIDwsr

one
three
1: one
2: two

正如你所看到的,Range 方法接受一个 func(key, value interface{}) bool 函数作为参数。如果我们返回 false,则迭代会停止。有趣的是,即使我们在恒定时间之后返回 false(更多信息),最坏情况下的时间复杂度仍然保持为 O(n)。

何时应该使用 sync.Map 而不是在经典的 map 上加 sync.Mutex 呢?

  • 当我们有频繁读取和不经常写入时(与 sync.RWMutex 类似)
  • 多个 goroutine 为不相交的键集合读取、写入和覆盖条目。这具体意味着什么?例如,如果我们有一个分片实现,有 4 个 goroutine 每个负责 25% 的键(没有冲突)。在这种情况下,sync.Map 也是首选。

sync.Pool

sync.Pool 是一个并发池,负责安全地保存一组对象

其公共方法包括:

  • Get() interface{} 用于检索一个元素
  • Put(interface{}) 用于添加一个元素
pool := &sync.Pool{}

pool.Put(NewConnection(1))
pool.Put(NewConnection(2))
pool.Put(NewConnection(3))

connection := pool.Get().(*Connection)
fmt.Printf("%d\n", connection.id)
connection = pool.Get().(*Connection)
fmt.Printf("%d\n", connection.id)
connection = pool.Get().(*Connection)
fmt.Printf("%d\n", connection.id)
1
3
2

值得注意的是,就顺序而言是没有保证的。Get 方法指定它从池中获取一个任意的项目。

也可以指定一个创建方法:

pool := &sync.Pool{
  New: func() interface{} {
    return NewConnection()
  },
}

connection := pool.Get().(*Connection)

每次调用 Get() 时,它将返回由传递给 pool.New 的函数创建的对象(在本例中是一个指针)。

何时应该使用 sync.Pool 呢?有两种情况:

第一种情况是当我们需要重用共享且长期存在的对象时,比如一个数据库连接。

第二种情况是优化内存分配

让我们考虑一个函数的示例,该函数将数据写入缓冲区并将结果持久化到文件中。使用 sync.Pool,我们可以重复使用分配给缓冲区的空间,跨不同的函数调用重复使用同一个对象。

第一步是检索先前分配的缓冲区(或者如果是第一次调用,则创建一个,但这已经被抽象化了)。然后,延迟操作是将缓冲区放回池中。

func writeFile(pool *sync.Pool, filename string) error {
	// Gets a buffer object
	buf := pool.Get().(*bytes.Buffer)
	// Returns the buffer into the pool
	defer pool.Put(buf)

	// Reset buffer otherwise it will contain "foo" during the first call
	// Then "foofoo" etc.
	buf.Reset()

	buf.WriteString("foo")

	return ioutil.WriteFile(filename, buf.Bytes(), 0644)
}

sync.Pool 还有一个要提到的重要点。由于指针可以被放入 Get() 返回的接口值中,无需进行任何分配,因此最好将指针放入池中而不是结构体。

这样,我们既可以有效地重用已分配的内存,又可以减轻垃圾收集器的压力,因为如果变量逃逸到堆上,它就不需要再次分配内存。

sync.Once

sync.Once 是一个简单而强大的原语,用于确保一个函数只被执行一次

在这个例子中,将只有一个 goroutine 显示输出消息:

once := &sync.Once{}
for i := 0; i < 4; i++ {
	i := i
	go func() {
		once.Do(func() {
			fmt.Printf("first %d\n", i)
		})
	}()
}

我们使用了 Do(func()) 方法来指定只有这部分代码必须被执行一次。

sync.Cond

让我们以最可能最少使用的原语 sync.Cond 结束。

它用于向 goroutine 发出信号(一对一)或向 goroutine(s) 广播信号(一对多)。

假设我们有一个场景,需要通知一个 goroutine 共享切片的第一个元素已被更新。

创建一个 sync.Cond 需要一个 sync.Locker 对象(可以是 sync.Mutexsync.RWMutex):

cond := sync.NewCond(&sync.RWMutex{})

接下来,让我们编写一个函数来显示切片的第一个元素:

func printFirstElement(s []int, cond *sync.Cond) {
	cond.L.Lock()
	cond.Wait()
	fmt.Printf("%d\n", s[0])
	cond.L.Unlock()
}

正如你所看到的,我们可以使用 cond.L 来访问内部互斥锁。一旦锁被获取,我们调用 cond.Wait(),它会阻塞直到收到信号。

现在回到主 goroutine。我们将通过传递一个共享切片和之前创建的 sync.Cond 来创建一个 printFirstElement 池。然后,我们调用一个 get() 函数,将结果存储在 s[0] 中并发出一个信号:

s := make([]int, 1)
for i := 0; i < runtime.NumCPU(); i++ {
	go printFirstElement(s, cond)
}

i := get()
cond.L.Lock()
s[0] = i
cond.Signal()
cond.L.Unlock()

这个信号将解除一个创建的 goroutine 的阻塞状态,它将显示 s[0]

然而,如果我们退一步来看,我们可能会认为我们的代码可能违反了 Go 最基本的原则之一:

不要通过共享内存来通信;相反,通过通信来共享内存。

事实上,在这个例子中,最好使用一个通道来传递 get() 返回的值。

然而,我们也提到了 sync.Cond 还可以用于广播信号

让我们修改上一个示例的结尾,将 Signal() 改为 Broadcast()

i := get()
cond.L.Lock()
s[0] = i
cond.Broadcast()
cond.L.Unlock()

在这种情况下,所有的 goroutine 都会被触发。

众所周知,通道元素只会被一个 goroutine 捕获。唯一模拟广播的方式是关闭一个通道,但这不能重复使用。因此,尽管 颇具争议,这无疑是一个有趣的特性。

还有一个值得提及的 sync.Cond 使用场景,也许是最重要的一个:

示例的 Go Playground 地址:https://play.golang.org/p/ap5qXF5DAg5

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1255502.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深入了解Java8新特性-日期时间API:LocalTime类

阅读建议 嗨&#xff0c;伙计&#xff01;刷到这篇文章咱们就是有缘人&#xff0c;在阅读这篇文章前我有一些建议&#xff1a; 本篇文章大概12000多字&#xff0c;预计阅读时间长需要10分钟。本篇文章的实战性、理论性较强&#xff0c;是一篇质量分数较高的技术干货文章&…

人力资源管理后台 === 员工新增修改

目录 1.员工管理-导出excel 2.员工管理-excel组件封装 3.员工管理-下载导入模板 4.员工管理-员工导入-上传excel 5.员工管理-删除员工 6.员工详情和路由 7.员工详情-表单数据校验 8.员工详情-封装部门级联组件 9.员工详情-级联组件-双向绑定 10.员工详情-新增员工 11…

深度学习中的注意力机制:原理、应用与实践

深度学习中的注意力机制&#xff1a;原理、应用与实践 摘要&#xff1a; 本文将深入探讨深度学习中的注意力机制&#xff0c;包括其原理、应用领域和实践方法。我们将通过详细的解析和代码示例&#xff0c;帮助读者更好地理解和应用注意力机制&#xff0c;从而提升深度学习模…

Servlet知识汇总

小王学习录 Ⅰ. servlet程序导入依赖创建目录编写代码打包程序部署程序验证简化打包和部署Ⅱ. Servlet Api1. HttpServlet2. HttpServletRequest3. HttpServletResponseⅢ. Cookie 和session1. Cookie2. Session(会话)3. session 和 cookie的区别4. session 和 cookie的联系5. …

图像分割模型及架构选型介绍(MMSegmentation|sssegmentation等)

参考&#xff1a; https://zhuanlan.zhihu.com/p/618226513 0. 图像分割概述 图像分割通过给出图像中每个像素点的标签&#xff0c;将图像分割成若干带类别标签的区块&#xff0c;可以看作对每个像素进行分类。图像分割是图像处理的重要组成部分&#xff0c;也是难点之一。随…

Java 基于 SpringBoot+Vue 的地方民宿管理系统

文章目录 开发环境&#xff1a;后端&#xff1a;前端&#xff1a;数据库&#xff1a; 系统架构&#xff1a;管理员&#xff1a;用户用例&#xff1a; 主要功能&#xff1a;用户功能模块管理员功能模块商家用户功能模块前台首页功能模块 源码咨询 大家好&#xff0c;今天为大家带…

如何保证线程安全?

程序员的公众号&#xff1a;源1024&#xff0c;获取更多资料&#xff0c;无加密无套路&#xff01; 最近整理了一波电子书籍资料&#xff0c;包含《Effective Java中文版 第2版》《深入JAVA虚拟机》&#xff0c;《重构改善既有代码设计》&#xff0c;《MySQL高性能-第3版》&…

Pytorch项目的文件结构一般都是怎么组织的?

如果是从一些比较典型的论文里弄下来的源码&#xff0c;你会发现它们的论文结构往往都非常复杂。不同的模型、不同的论文&#xff0c;可能代码结构组织的方式都不一样。但它们都不外乎就是经历这几个方面&#xff1a; 1、模型和结构模块定义&#xff1b; 2、数据集获取与处理…

【brpc学习实践九】mbvar及bvar可观测

概念、学习地址 mbvar中有两个类&#xff0c;分别是MVariable和MultiDimension&#xff0c;MVariable是多维度统计的基类&#xff0c;MultiDimension是派生模板类。也是主要用来多多线程计数用的。这里用到再详细去了解即可 https://github.com/luozesong/brpc/blob/master/do…

5.golang字符串的拆解和拼接

字符串是 Go 中的字节切片。可以通过将一组字符括在双引号中来创建字符串" "。Go 中的字符串是兼容Unicode编码的&#xff0c;并且是UTF-8编码的。 访问字符串的单个字节或字符 由于字符串是字节切片&#xff0c;因此可以访问字符串的每个字节。 func printStr(s …

知识的分层:know-what/how/why

知识&#xff08;knowledge&#xff09;表示知道某些信息。通常而言&#xff0c;知识是正确的&#xff0c;但不一定是完备的。知识本身有自己的适用范围&#xff0c;特别是工程技术类问题。 根据知识的类型&#xff0c;可分为三类&#xff1a; know-whatknow-howknow-why kno…

Redis 面试题——持久化

目录 1.概述1.1.Redis 的持久化功能是指什么&#xff1f;1.2.Redis 有哪些持久化机制&#xff1f; 2.RDB2.1.什么是 RDB 持久化&#xff1f;2.2.Redis 中使用什么命令来生成 RDB 快照文件&#xff1f;2.3.如何在 Redis 的配置文件中对 RDB 进行配置&#xff1f;2.4.✨RDB 持久化…

代码随想录算法训练营 ---第四十四天

今天开始《动态规划&#xff1a;完全背包》的学习&#xff01; 前言&#xff1a; 完全背包和01背包的区别在于完全背包里的物品能无限次使用&#xff0c;01背包只能用一次。 第一题&#xff1a; 简介&#xff1a; 本题是纯完全背包的使用。可以看一看和01背包的区别。 代码…

手把手教你实现一个循环队列(C语言)

这是一道leetcode关于队列的经典题&#xff1a; 622. 设计循环队列https://leetcode.cn/problems/design-circular-queue/ 思路&#xff1a; 大家注意这个题目要求&#xff0c;这个队列是定长的&#xff0c;如果满了则不能再添加数据。那么我们设计一个队头front和队尾rear&…

单片机学习4——中断的概念

中断的概念&#xff1a; CPU在处理A事件的时候&#xff0c;发生了B事件&#xff0c;请求CPU迅速去处理。&#xff08;中断产生&#xff09; CPU暂时中断当前的工作&#xff0c;转去处理B事件。&#xff08;中断响应和中断服务&#xff09; 待CPU将B事件处理完毕后&#xff0…

Java王者荣耀小游戏

Background类 package LX;import java.awt.*; //背景类 public class Background extends GameObject{public Background(GameFrame gameFrame) {super(gameFrame);}Image bg Toolkit.getDefaultToolkit().getImage("C:\\Users\\ASUS\\Desktop\\王者荣耀图片\\Map.jpg&…

基于51单片机的音乐喷泉设计

**单片机设计介绍&#xff0c;基于51单片机的音乐喷泉设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于51单片机的音乐喷泉设计是一种将音乐和水喷射效果相结合的创意设计。下面是一个简要的设计介绍&#xff1a; 硬件设…

003、ArkTS开发实践

之——尝试 杂谈 学习声明式UI语法&#xff1a; 正文 1.声明式UI 1.1 声明式描述 想要什么样子就直接描述&#xff1a; 1.2 状态驱动视图更新 2.自定义组件 对页面内容进行合理抽象&#xff0c;组合基础组件&#xff0c;封装成自定义组件。 自定义子组件&#xff0c;为后续使…

Leetcode211. 添加与搜索单词 - 数据结构设计

Every day a Leetcode 题目来源&#xff1a;211. 添加与搜索单词 - 数据结构设计 解法1&#xff1a;字典树 字典树&#xff08;前缀树&#xff09;是一种树形数据结构&#xff0c;用于高效地存储和检索字符串数据集中的键。前缀树可以用 O(∣S∣) 的时间复杂度完成如下操作…

【Vue】记事本

上一篇&#xff1a;Vue的指令 https://blog.csdn.net/m0_67930426/article/details/134599378?spm1001.2014.3001.5501 本篇所需指令&#xff1a; v- for v-model v-on v-show 目录 删除功能 添加功能 统计功能 清空功能 v-show 删除功能 <!DOCTYPE html> …