GO 语言处理并发的时候我们是选择sync还是channel

news2025/2/1 8:52:24

以前写 C 的时候,我们一般是都通过共享内存来通信,对于并发去操作某一块数据时,为了保证数据安全,控制线程间同步,我们们会去使用互斥锁,加锁解锁来进行处理

然而 GO 语言中建议的时候通过通信来共享内存,使用 channel 来完成临界区的同步机制

可是 GO 语言中的 channel 毕竟是属于比较高级的原语,自然在性能上就比不上 sync包里面的锁机制,感兴趣的同学可以自己写一个简单的基准测试来确认一下效果,评论去可以交流

另外,使用 sync 包来控制同步时,我们不会失去结构对象的所有权,还能让多个协程之间同步访问临界区的资源,那么如果我们的需求能够符合这种情况时,还是建议使用 sync 包来控制同步更加的合理和高效

为什么会选择使用 sync 包来控制同步结论:

  1. 不期望失去结构的控制权的同时,还期望多个协程能够安全的同步访问临界区资源
  2. 对性能要求会更高的情况

sync 的 Mutex 和 RWMutex

查看 sync 包的源码(xxx\Go\src\sync),我们可以看到 sync 包下面有如下几个结构:

  1. Mutex
  2. RWMutex
  3. Once
  4. Cond
  5. Pool
  6. atomic 包原子操作

上述经常使用的就是 Mutex 了,尤其是最开始不善于使用 channel 的时候,觉得使用 Mutex 非常的顺手,其次 RWMutex 相对来说就会用的少一些

不知大家有没有关注过,使用 Mutex 和 使用 RWMutex 的性能表现,获取大部分人都是默认使用互斥锁,一起写个 demo 来看看 他俩的性能对比

var (
        mu   sync.Mutex
        murw sync.RWMutex
        tt1  = 1
        tt2  = 2
        tt3  = 3
)

// 使用 Mutex 控制读取数据
func BenchmarkReadMutex(b *testing.B) {
        b.RunParallel(func(pp *testing.PB) {
                for pp.Next() {
                        mu.Lock()
                        _ = tt1
                        mu.Unlock()
                }
        })
}

// 使用 RWMutex 控制读取数据
func BenchmarkReadRWMutex(b *testing.B) {
        b.RunParallel(func(pp *testing.PB) {
                for pp.Next() {
                        murw.RLock()
                        _ = tt2
                        murw.RUnlock()
                }
        })
}

// 使用 RWMutex 控制读写入数据
func BenchmarkWriteRWMutex(b *testing.B) {
        b.RunParallel(func(pp *testing.PB) {
                for pp.Next() {
                        murw.Lock()
                        tt3++
                        murw.Unlock()
                }
        })
}

写了三个简单的基准测试

  1. 使用互斥锁读取数据
  2. 使用读写锁的读锁读取数据
  3. 使用读写锁读取和写入数据
$ go test -bench . bbb_test.go --cpu 2
goos: windows
goarch: amd64
cpu: Intel(R) Core(TM)2 Duo CPU     T7700  @ 2.40GHz
BenchmarkReadMutex-2            39638757                30.45 ns/op
BenchmarkReadRWMutex-2          43082371                26.97 ns/op
BenchmarkWriteRWMutex-2         16383997                71.35 ns/op


$ go test -bench . bbb_test.go --cpu 4
goos: windows
goarch: amd64
cpu: Intel(R) Core(TM)2 Duo CPU     T7700  @ 2.40GHz
BenchmarkReadMutex-4            17066666                73.47 ns/op
BenchmarkReadRWMutex-4          43885633                30.33 ns/op
BenchmarkWriteRWMutex-4         10593098               110.3 ns/op


$ go test -bench . bbb_test.go --cpu 8
goos: windows
goarch: amd64
cpu: Intel(R) Core(TM)2 Duo CPU     T7700  @ 2.40GHz
BenchmarkReadMutex-8             8969340               129.0 ns/op
BenchmarkReadRWMutex-8          36451077                33.46 ns/op
BenchmarkWriteRWMutex-8          7728303               158.5 ns/op



$ go test -bench . bbb_test.go --cpu 16
goos: windows
goarch: amd64
cpu: Intel(R) Core(TM)2 Duo CPU     T7700  @ 2.40GHz
BenchmarkReadMutex-16            8533333               132.6 ns/op
BenchmarkReadRWMutex-16         39638757                29.98 ns/op
BenchmarkWriteRWMutex-16         6751646               173.9 ns/op



$ go test -bench . bbb_test.go --cpu 128
goos: windows
goarch: amd64
cpu: Intel(R) Core(TM)2 Duo CPU     T7700  @ 2.40GHz
BenchmarkReadMutex-128          10155368               116.0 ns/op
BenchmarkReadRWMutex-128        35108558                33.27 ns/op
BenchmarkWriteRWMutex-128        6334021               195.3 ns/op

可以看出来当并发较小的时候,使用互斥锁和使用读写锁的读锁性能类似,当并发逐渐变大时,读写锁的读锁性能并未发生较大变化,互斥锁和读写锁的性能都会随着并发的变大而下降

那么很明显,读写锁适用于读多写少的场景,在大并发读书数据的时候,多个协程可以同时拿到读锁,减少锁竞争和等待时间

而互斥锁并发的时候,多个协程中,只有一个协程能拿到锁,其他协程就会阻塞和等待,影响性能

举个例子,我们正常使用互斥锁,看看可能会出现什么样的问题

使用 sync 需要注意的地方

平时使用 sync 包中的锁的时候,需要注意的是不要去拷贝已经已经使用过的 Mutex 或者是 RWMutex

写一个简单的 demo:

var mu sync.Mutex

// sync 的互斥锁,读写锁,在被使用之后,就不要去复制这个对象,若要复制,需要在其未被使用的时候
func main() {

    go func(mm sync.Mutex) {
            for {
                    mm.Lock()
                    time.Sleep(time.Second * 1)
                    fmt.Println("g2")
                    mm.Unlock()
            }
    }(mu)

    mu.Lock()
    go func(mm sync.Mutex) {
            for {
                    mm.Lock()
                    time.Sleep(time.Second * 1)
                    fmt.Println("g3")
                    mm.Unlock()
            }
    }(mu)

    time.Sleep(time.Second * 1)
    fmt.Println("g1")

    mu.Unlock()

    time.Sleep(time.Second * 20)
}

感兴趣的朋友的,可以运行一下,可以看到打印的结果中时没有 g3 的,因此 g3 所在的协程已经发生了死锁,没有机会去调用 unlock

出现这种情况的原因是这样的,先来看看 Mutex 的内部结构:

//...
// A Mutex must not be copied after first use.
//...
type Mutex struct {
        state int32
        sema  uint32
}

因为例如 Mutex 中的内部结构是有一个 state (表示互斥锁的状态)和 sema(表示控制互斥锁的信号量),其中初始化 Mutex 的时候,他们都是 0,但是当我们用 Mutex 加锁时,Mutex 的状态就变成了 Locked 的状态,这个时候,其中一个协程去拷贝这个 Mutex,并在自己协程中加锁,就会出现死锁的情况,这一点是非常需要注意的

如果涉及到这种多个协程使用 Mutex 的情况, 可以使用闭包或者传入包裹锁的结构地址或者指针,这样就可以避免使用锁的时候导致不可预期的结果,避免一脸蒙圈

sync.Once

sync 包中的其他成员,不知 xdm 使用的多么,相对使用频率较高的应该就是 sync.Once 了,其他成员 xdm 可以自行看看源码,或者评论区留言哦,我们来看看 syn.Once 如何使用,都有哪些需要注意的?

还记得之前写 C 或者 C++ 的时候,对于程序生命周期只有一个实例的时候,我们会选择使用单例模式来进行处理,那么此处的 sync.Once 就是非常适合用在单例模式中

sync.Once 可以保证任意一个函数在程序运行期间只被执行一次,这一点相对来说就比每个包中的 init 函数灵活一些了

这里需要注意,sync.Once 中执行的函数,如果出现了 panic ,也是会被认为是执行完了了一次,之后如果再有逻辑需要进入 sync.Once 是无法进入并执行函数逻辑的

一般情况下, sync.Once 用于对象资源的初始化和清理动作,避免重复操作,可以来看一个 demo:

  1. 主函数开辟 3 个协程,且使用 sync.WaitGroup 来管控并等待子协程退出
  2. 主函数开辟所有协程之后等待 2 秒,开始创建并获取实例
  3. 协程中也在获取实例
  4. 只要有一个协程获取到进入 Once,执行逻辑之后,会出现 panic
  5. 出现 panic 的协程捕获了异常,此时全局的 instance 已经被初始化,其他协程仍然无法进入 Once 内的函数
type Instance struct {
        Name string
}

var instance *Instance
var on sync.Once

func GetInstance(num int) *Instance {

        defer func() {
                if err := recover(); err != nil {
                        fmt.Println("num %d ,get instance and catch error ... \n", num)
                }
        }()

        on.Do(func() {
                instance = &Instance{Name: "阿兵云原生"}
                fmt.Printf("%d enter once ... \n", num)
                panic("panic....")
        })

        return instance
}

func main() {

        var wg sync.WaitGroup
        for i := 0; i < 3; i++ {
                wg.Add(1)
                go func(i int) {
                        ins := GetInstance(i)
                        fmt.Printf("%d: ins:%+v  , p=%p\n", i, ins, ins)
                        wg.Done()
                }(i)
        }

        time.Sleep(time.Second * 2)

        ins := GetInstance(9)
        fmt.Printf("9: ins:%+v  , p=%p\n", ins, ins)
        wg.Wait()
}

通过打印结果可以看出,0 对应的协程进入了 Once,且发生了 panic,因此当前协程获取到的 GetInstance 函数的结果是 nil

其他的协程包括主协程调用 GetInstance 函数都能正常拿到 instance 的地址,可以看出地址是同一个,全局就只初始化了一次

$ go run main.go
0 enter once ...
num %d ,get instance and catch error ...
 0
0: ins:<nil>  , p=0x0
1: ins:&{Name:阿兵云原生}  , p=0xc000086000
2: ins:&{Name:阿兵云原生}  , p=0xc000086000
9: ins:&{Name:阿兵云原生}  , p=0xc000086000

总结

  1. 如何选择 sync 和 channel
  2. sync 锁的使用注意事项
  3. sync 互斥锁和读写锁的性能对比
  4. sync Once 的使用演示

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

好了,本次就到这里

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是阿兵云原生,欢迎点赞关注收藏,下次见~
可以进入地址进行体验和学习:https://xxetb.xet.tech/s/3lucCI

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

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

相关文章

看图学习数据中心机柜导轨方面的英文术语

对于一些数据中心的服务器&#xff0c;存储&#xff0c;交换机等设备的上架安装&#xff0c;有些导轨设计还是有点复杂的&#xff0c;如果安装手册还是英文的&#xff0c;就更有些挑战了。本文用一个实际的导轨图片来对其中常用的一些英文术语做了一个详细解释&#xff0c;供朋…

面试官:Go GMP 模型为什么 P 组件如此重要 ?

大家好&#xff0c;我是木川 Go GMP模型 是 Go语言并发性能的关键组成部分&#xff0c;它允许轻松创建大量的 Goroutines&#xff0c;设计思想包括并行性、线程复用以及抢占调度。 Go 1.1 版本前采用的是 GM 模型&#xff0c;存在一些问题&#xff0c;后面增加了 P 组件&#x…

哪种烧录单片机的方法合适?

哪种烧录单片机的方法合适&#xff1f; 首先&#xff0c;让我们来探讨一下单片机烧录的方式。虽然单片机烧录程序的具体方法会因为单片机型号、然后很多小伙伴私我想要嵌入式资料&#xff0c;通宵总结整理后&#xff0c;我十年的经验和入门到高级的学习资料&#xff0c;只需一…

计算机算法分析与设计(9)---0-1背包和完全背包问题(含C++代码)

文章目录 一、0-1背包概述1.1 问题描述1.2 算法思想 二、0-1背包代码2.1 题目描述2.2 代码编写 三、完全背包概述四、完全背包代码4.1 题目描述4.1 代码编写4.2 代码优化 一、0-1背包概述 1.1 问题描述 1. 0-1背包问题&#xff1a;给定 n n n 种物品和一背包。物品 i i i 的…

Unity可视化Shader工具ASE介绍——7、ASE实现Matcap效果和自定义节点

大家好&#xff0c;我是阿赵。继续介绍Unity可视化Shader编辑工具ASE。上一篇用了很长的篇幅来做了一个遮挡X光的效果。这一篇来做一个MatCap效果。不过做MatCap并不是目的&#xff0c;是想说明一下&#xff0c;怎样在ASE里面自定义方法节点。 一、在ASE里面做MatCap材质 由于…

【每日一题Day348】LC137只出现一次的数字Ⅱ | 状态转移

只出现一次的数字Ⅱ【LC137】 给你一个整数数组 nums &#xff0c;除某个元素仅出现 一次 外&#xff0c;其余每个元素都恰出现 **三次 。**请你找出并返回那个只出现了一次的元素。 你必须设计并实现线性时间复杂度的算法且不使用额外空间来解决此问题。 排序 将数组从小到大…

LED灯的基本控制

题目要求 首先让8路LED指示灯闪烁3遍然后熄灭&#xff0c;接着依次点亮LED指示灯&#xff0c;最后依次熄灭LED灯&#xff0c;程序循环实现上述功能。 分析设计 原理图&#xff1a; 首先应该选通Y4C&#xff0c;也就是ABC 100。 代码 #include <regx52.h> #define…

C语言 —— 指针

目录 1. 指针是什么&#xff1f; 2. 指针和指针类型的关系 2.1 指针的解引用 2.2 指针-整数 3. 野指针 3.1 野指针成因 1. 指针未初始化 2. 指针越界访问 3. 指针指向的空间释放 3.2 如何规避野指针 4. 指针运算 4.1 指针-整数 4.2 指针-指针 指针-指针的使用 4.3 指针的关系运…

3.7 static关键字

思维导图&#xff1a; 3.7.1 静态属性 ### 3.7 static关键字 --- Java提供了一个称为static的关键字&#xff0c;用于修饰类的成员&#xff0c;如成员变量、成员方法和代码块。使用static修饰的成员拥有特殊性。 --- #### 3.7.1 静态属性 当属性被static关键字修饰时&…

嵌入式实时操作系统的设计与开发(轮询系统学习)

轮询系统具有以下工作特点&#xff1a;系统完成一个轮询的时间取决于循环中需要执行的函数个数。此外&#xff0c;轮询的次序是静态固定的&#xff0c;在运行时不能进行动态调整。 典型系统 许多工业线程网络中&#xff0c;由于需要控制的设备较多、相互距离又较远&#xff0…

手把手教你用Python绘制神经网络图

接下来教大家如何使用 Python 中的 networkx 库&#xff0c;绘制美观且标准的神经网络。会根据指定的层和节点数量&#xff0c;绘制不同结构的神经网络。 networkx 库可以用来创建和操作图类型的数据结构&#xff0c;其中包括无向图、有向图、带权图等等。 神经网络可以看做是一…

字节码之 Lambda 表达式底层原理

文章目录 0.前言0. lambda程序示例1. 编译程序&#xff1a;2. 使用 javap 分析字节码3. 输出字节码4. 分析指令 1. Lambda 表达式的字节码实现1.1 什么是invokedynamic 指令invokedynamic 的工作原理为何 invokedynamic 如此特殊&#xff1f; 1.2 bootstrap method 详解1.1 Lam…

Qt之给控件添加右键菜单

一、设置控件 在对应控件的属性中&#xff0c;将contextMenuPolicy设置为CustomContextMenu。 二、添加槽函数 在对应控件上右键选择槽函数customContextMenuRequested(QPoint)。 三、在槽函数中添加右键菜单 在槽函数中输入如下代码&#xff0c;添加右键菜单。 //右键菜单 …

红帽Linux的安装和部署

目录 一、红帽Linux的安装阶段 1、下载redhat7.9的iso镜像 2、安装阶段 二、红帽Linux的配置阶段 1、第一次进入装机配置 2、进入机器后的一些配置 三、远程连接阶段 1、关闭防火墙 2、使用Xshell远程连接&#xff08;其他连接工具也行&#xff09; 1.开启SSH服务 2.连…

二十、【钢笔工具组】

文章目录 钢笔工具自由钢笔工具弯度钢笔工具 钢笔工具 钢笔工具在photoshop作图中是一款使用频率较高的路径工具,我们可以在窗口选项栏中将路径编辑栏打开&#xff0c;如果我们需要选中使用路径&#xff0c;需要用到后边的路径工具才能去拖动&#xff0c;而选择工具不能拖动&a…

9月大型语言模型研究论文总结

大型语言模型(llm)在今年发展迅速&#xff0c;随着新一代模型不断地被开发&#xff0c;研究人员和工程师了解最新进展变得非常重要。本文总结9-10月期间发布了一些重要的LLM论文。 这些论文涵盖了一系列语言模型的主题&#xff0c;从模型优化和缩放到推理、基准测试和增强性能…

Sigma中的数字增益放大/降低方法

1 是否需要申请加入数字音频系统研究开发交流答疑群(课题组)&#xff1f;加他微信hezkz17, 本群提供音频技术答疑服务

如何快速分析一款产品?

一、何时需要对一个产品进行分析&#xff1f; 首先&#xff0c;当你刚刚融入一个新的产品团队&#xff0c;尤其是当你需要深入了解你将负责的产品时&#xff0c;分析产品就显得尤为重要。这有助于你对产品的全面理解&#xff0c;发现其中的优势和不足&#xff0c;为未来的工作提…

14.5 Socket 应用组播通信

组播通信是一种基于UDP协议的网络通信方式&#xff0c;它允许发送方将消息同时传递给多个接收方。在组播通信中&#xff0c;发送方和接收方都会加入一个共同的组播组&#xff0c;这个组播组对应一个特定的IP地址&#xff0c;所有加入该组播组的主机都能够接收到发送方发送的消息…

C++概述

一、C特色 1.C是面向对象的高级程序设计语言 2.支持数据封装&#xff0c;将数据和对该数据进行操作的函数封装在一个类中&#xff0c;对象就是某一个具体的类。即类是数据封装的工具&#xff0c;对象是数据封装的实现。 3.具有继承性 4.具有函数重载 二、拓展介绍 1.C标准&a…