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

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 somethingwg.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 objectbuf := pool.Get().(*bytes.Buffer)// Returns the buffer into the pooldefer 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 := igo 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.mzph.cn/news/189738.shtml

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

相关文章

Jinja2使用Layui报 “d is not defined“

问题出现场景在使用Jinja2渲染Layui的表格时候&#xff0c;要做自定义templte的传入 Jinja2这块本来就是支持 {{ }} 插值的模板语言&#xff0c;所以这块的第一种渲染方式会冲突 所以只能用函数返回代码块进行填充&#xff0c;不能使用插值&#xff0c;只能拼接字符串 templt…

Gradle windows下配置

1.Gradle下载 打开官网下载界面&#xff1a;https://gradle.org/releases/ 如果你使用的SpringBoot项目&#xff0c;建议使用6.8及以上的版本 2.下载后放到目录下 3.配置环境变量 配置gradle_home 配置Path 4.配置成功 5.配置国内源 新建一个init.gradle文件&#xff0c;配…

MySQL- CRUD-单表查询

一、INSERT 添加 公式 INSERT INTO table_name [(column [, column...])] VALUES (value [, value...]); 示例&#xff1a; CREATE TABLE goods (id INT ,good_name VARCHAR(10),price DOUBLE ); #添加数据 INSERT INTO goods (id,good_name,price ) VALUES (20,华为手机,…

虚假IP地址攻击的溯源方法

随着网络技术的迅速发展&#xff0c;网络攻击行为也日益猖獗。其中&#xff0c;虚假IP地址攻击是一种较为常见的网络攻击方式&#xff0c;它利用虚假的IP地址&#xff0c;通过互联网对目标进行攻击和入侵。这种攻击方式不仅难以追踪&#xff0c;而且往往会给企业和个人带来巨大…

股票要怎么买入卖出?

股票账户终于开好了&#xff01;恭喜你马上就可以开启刺激的炒股之旅了&#xff01;不过第一次买股票的你是不是还不知道怎么个买法呢&#xff1f;别担心~贴心的汇小鲸带着教程来了&#xff0c;咱们一起看看吧&#xff01; 首先一点&#xff0c;大家得知道&#xff1a;开好户还…

JavaFramework JDK Version Test

测试JDK8 JDK17编译包 当前环境JDK8 CASE 1&#xff1a; /*** * author ZengWenFeng* email 117791303QQ.com* mobile 13805029595* date 2023-08-07*/ package zwf;import a.T; import ce.pub.util.GUID;/*** 测试高版本JDK编译JAR&#xff0c;低版本错误** author ZengWenF…

震坤行:数字驱动食品农副行业采购的新兴趋势与实践

震坤行&#xff1a;数字驱动食品农副行业采购的新兴趋势与实践 近年来消费者对于营养价值和健康的追求日益凸显&#xff0c;促使各类有机食品、低糖低脂食品、素食等健康食品受到热烈追捧。同时&#xff0c;以往单一的产品也被各家企业“卷”出了个性化&#xff0c;光是卖水&a…

技术经济与企业管理 救命稻草

明天要考试了&#xff0c;今天开始预习&#xff01; 此课也叫【化工技术经济】、【工程经济学】 目录 第二章 企业战略分析市场预测方法一元线性回归方法简单移动平均法 第三章 资金的时间价值&#xff08;考&#xff09;资金时间价值的衡量利率现金流量图资金等值计算&#x…

四、设置主机名和域名映射

目录 1、配置每台虚拟机主机名 2、配置每台虚拟机域名映射 1、配置每台虚拟机主机名

windows ssh时出现Bad local forwarding specification的解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

掌握HarmonyOS框架的ArkTs如何管理和共享状态数据

ARKTS&#xff08;Ark TypeScript&#xff09;是HarmonyOS应用框架的一部分&#xff0c;提供了一种灵活而强大的状态管理机制。在ARKTS中&#xff0c;AppStorage和LocalStorage是两个关键的概念&#xff0c;它们分别用于应用级和页面级的状态共享。通过深入了解这两个特性&…

2023年建筑轮廓高度数据技术服务

在之前的文章【数据分享】全国62个城市建筑轮廓数据带层数&#xff08;2018年&#xff09;和【数据分享】全国77个城市建筑轮廓数据带高度&#xff08;2019年&#xff09;里我们分别分享了2018年和2019年2020年2021年2022年的建筑轮廓数据&#xff0c;数据年份也有些老了&#…

计算机网络——数据链路层-封装成帧(帧定界、透明传输-字节填充,比特填充、MTU)

目录 介绍 帧定界 PPP帧 以太网帧 透明传输 字节填充&#xff08;字符填充&#xff09; 比特填充 比特填充习题 MTU 介绍 所谓封装成帧&#xff0c;就是指数据链路层给上层交付下来的协议数据单元添加帧头和帧尾&#xff0c;使之成为帧。 例如下图所示&#xff1a; …

蓝桥杯每日一题2023.12.1

题目描述 蓝桥杯大赛历届真题 - C 语言 B 组 - 蓝桥云课 (lanqiao.cn) 题目分析 对于此题目而言思路较为重要&#xff0c;实际可以转化为求两个数字对应的操作&#xff0c;输出最前面的数字即可 #include<bits/stdc.h> using namespace std; int main() {for(int i 1…

图解java.util.concurrent并发包源码系列——深入理解定时任务线程池ScheduledThreadPoolExecutor

深入理解定时任务线程池ScheduledThreadPoolExecutor ScheduledThreadPoolExecutor作用与用法ScheduledThreadPoolExecutor内部执行流程DelayedWorkQueueScheduledFutureTask源码分析任务提交ScheduledFutureTask的属性和方法delayedExecute(t) 任务执行ScheduledFutureTask.su…

基于机器深度学习的交通标志目标识别

在线工具推荐&#xff1a; 三维数字孪生场景工具 - GLTF/GLB在线编辑器 - Three.js AI自动纹理化开发 - YOLO 虚幻合成数据生成器 - 3D模型在线转换 - 3D模型预览图生成服务 智能交通系统&#xff08;ITS&#xff09;&#xff0c;包括无人驾驶车辆&#xff0c;尽管在道路…

安装两个WIN10/WIN11系统到两个盘中,第二个系统依赖原系统盘引导的问题

前段时间折腾装一个双系统&#xff0c;主要两个方面考虑&#xff1a; 1. 原来的系统又许多软件&#xff0c;想着先保留&#xff1b; 2. 系统想安装到一个固态硬盘中&#xff1b; 在安装的过程中遇到了一些问题&#xff0c;这里记录分享一下。 问题1&#xff0c;运行系统自动安装…

Langchain-Chatchat学习

参考&#xff1a;Langchain-Chatchat 阿里通义千问Qwen 保姆级教程 | 次世代知识管理解决方案 - 知乎 (zhihu.com) 该文档没有安装成功&#xff0c;安装成功的文档 可见&#xff1a;Langchain-Chatchat的安装过程-CSDN博客 中文LLM生态观察 模型 就开源的部分而言&#xf…

Servlet概念视频笔记

学习地址:121-尚硅谷-Servlet-什么是Servlet_哔哩哔哩_bilibili 目录 1.Servlet技术 a.什么是Servlet b.手动实现Servlet程序 c.url地址如何定位到Servlet程序去访问 d.Servlet的生命周期 e.GET 和 POST 请求的分发处理 f.通过继承 HttpServlet 实现 Servlet程序 g.使用…

如何在财税行业查找批量客户?

现在市场上代记账公司也不算少&#xff0c;做过这行的都知道&#xff0c;最初呢行业竞争不强&#xff0c;都是靠地推、老客户转介绍&#xff0c;或者长期以往的蹲守各个地区的工商注册服务中心&#xff0c;找那些才注册企业的老板或者创业者。但是&#xff0c;随着市场经济的发…