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

以前写 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 的情况, 可以使用闭包或者传入包裹锁的结构地址或者指针,这样就可以避免使用锁的时候导致不可预期的结果,避免一脸蒙圈

[图片上传失败...(image-416f8d-1697332768197)]

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 的使用演示

欢迎点赞,关注,收藏

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

[图片上传失败...(image-8464b1-1697332768197)]

好了,本次就到这里

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

我是阿兵云原生,欢迎点赞关注收藏,下次见~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • 1.介绍 sync包提供了互斥锁。除了Once和WaitGroup类型,其余多数适用于低水平的程序,多数情况下,高...
    呦丶耍脾气阅读 308评论 0 0
  • go并发编程入门到放弃 并发和并行 并发:一个处理器同时处理多个任务。 并行:多个处理器或者是多核的处理器同时处理...
    yangyunfeng阅读 555评论 0 2
  • 1.介绍[https://go.liuqh.icu/#/%E5%9F%BA%E7%A1%80%E7%AF%87/1...
    呦丶耍脾气阅读 174评论 0 0
  • 锁 Mutex 互斥锁 互斥即不可同时运行。即使用了互斥锁的两个代码片段互相排斥,只有其中一个代码片段执行完成后,...
    Xuenqlve阅读 691评论 0 1
  • 原文链接:https://blog.csdn.net/chenguolinblog/article/details...
    逆水寻洲阅读 1,340评论 0 1