

Golang - sync包
source link: https://studygolang.com/articles/29009
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.


Golang的 sync包提供了大量的线程同步操作的类. 所以满足我们日常使用. 包含锁,和安全的集合. 其中,关于golang 各种锁的实现,请看这个文章,我个人感觉写的不错 juejin.im/entry/5ed32…
sync.Mutex
第一种就是 Mutex
为互斥锁 , 实现跟Java的reentrantlock很像. 基本都是自旋锁,
我们下面有一个场景 , 将变量x累加50000次 我们开启5个goroutine去执行
func main() { var x = 0 for num := 0; num < 4; num++ { go func() { for i := 0; i < 10000; i++ { increase(&x) } }() } // 等待子线程退出 time.Sleep(time.Millisecond * 2000) fmt.Println(x) } // 这个操作是Java做不了的.只有C,C++和Golang可以 func increase(x *int) { *x = *x + 1 } 复制代码
如果输出结果是 50000那就对了, 我们看看结果
23283 复制代码
为什么呢, 因为多线程同时操作一个变量时就会出现这种问题 , 出现读写不一致的问题 , 如何处理呢
我们申明一个锁,让他同步执行 , 这么申明就可以了
var ILock = sync.Mutex{} func main() { var x = 0 for num := 0; num < 5; num++ { go func() { for i := 0; i < 10000; i++ { increase(&x) } }() } // 等待子线程退出 time.Sleep(time.Millisecond * 2000) fmt.Println(x) } func increase(x *int) // 1. 先加锁 ILock.Lock() // 执行完方法释放锁. 跟Java的很像,但是不是reentrant defer ILock.Unlock() *x = *x + 1 } 复制代码
此时执行 发现结果
50000 复制代码
输出正确.
其实还可以这么写. 效率更高
go func() { ILock.Lock() defer ILock.Unlock() for i := 0; i < 10000; i++ { increase(&x) } }() 复制代码
利用Chan阻塞实现锁
var ( lock = make(chan int, 1) ) func Lock() { lock <- 1 } func Unlock() { <-lock } 复制代码
此时这就是一个最简单的锁.
sync.RWMutex 读写锁
跟Java的一模一样 , 我就不多说了, 读写互斥, 可多读(读锁可以同时存在多个), 但读写不能同时存在, 写互斥
func main() { // 读写锁 mutex := sync.RWMutex{} go func() { // 读锁 mutex.RLocker().Lock() fmt.Println("读") mutex.RLocker().Unlock() }() go func() { mutex.RLocker().Lock() fmt.Println("读") time.Sleep(time.Millisecond * 5000) mutex.RLocker().Unlock() }() time.Sleep(time.Millisecond * 1000) go func() { // 写锁 , 就是普通的锁了 fmt.Println("拿到了") mutex.Lock() fmt.Println("写") mutex.Unlock() }() time.Sleep(time.Millisecond * 6000) } 复制代码
输出
读 读 拿到了 // 耗时1ms 写 // 对比前一步耗时 ms 复制代码
sync.WaitGroup
这个类类似于Java的 CountDownLatch
类, 但是比Java的好点. 第一Java的需要初始化告诉他计数器是多少, 所以他只有一个countdown和wait操作.
但是 sync.WaitGroup
却是三个操作, 第一个 down , add(int) , wait 三个操作. 所以他实现更加好.
简单以第一个例子为例子吧, 比如说 , 我们不知道goroutine啥时候退出是吧, 但是有了这玩意就知道了.
func main() { var WG = sync.WaitGroup{} start := time.Now().UnixNano() / 1e6 for num := 0; num < 5; num++ { WG.Add(1) go func(num int) { defer WG.Done() ran := rand.Int63n(1000) fmt.Printf("goroutine-%d sleep : %dms\n", num, ran) time.Sleep(time.Millisecond * time.Duration(ran)) }(num) } // 等待子线程退出 WG.Wait() fmt.Printf("main waitting : %dms\n", time.Now().UnixNano()/1e6-start) } 复制代码
输出 : 根据木桶原理, 耗时最长的是937ms, 所以主线程等待了939ms.
goroutine-1 sleep : 410ms goroutine-0 sleep : 821ms goroutine-2 sleep : 51ms goroutine-3 sleep : 937ms goroutine-4 sleep : 551ms main waitting : 939ms 复制代码
注意点 :
由于 sync.WaitGroup
也是一个对象Structs, 所以需要指针传递, 不能使用值传递, 注意一下.因为状态值复制了就无效了.
根据封装, 我们需要传递 sync.waitgroup .
func fun(num int, wg *sync.WaitGroup) { defer wg.Done() ran := rand.Int63n(1000) fmt.Printf("goroutine-%d sleep : %dms\n", num, ran) time.Sleep(time.Millisecond * time.Duration(ran)) } 复制代码
然后main方法
func main() { var WG = &sync.WaitGroup{} start := time.Now().UnixNano() / 1e6 for num := 0; num < 5; num++ { WG.Add(1) go fun(num, WG) } // 等待子线程退出 WG.Wait() fmt.Printf("main waitting : %dms\n", time.Now().UnixNano()/1e6-start) } 复制代码
输出 :
goroutine-4 sleep : 410ms goroutine-1 sleep : 551ms goroutine-0 sleep : 821ms goroutine-2 sleep : 51ms goroutine-3 sleep : 937ms main waitting : 939ms 复制代码
sync.Once 一次操作
这个玩意可以让 once.Do()
方法只执行一次. 其实类似于一个flag,一开始为true. 每次执行判断是否为true , 当执行了一次以后改成false. 其实他就是这个原理 , 不过他使用了cas , 保证了线程安全性,
func main() { once := sync.Once{} one(&once) one(&once) one(&once) } func one(once *sync.Once) { fmt.Println("执行函数") once.Do(func() { fmt.Println("只会执行了一次") }) } 复制代码
输出
执行函数 只会执行了一次 执行函数 执行函数 复制代码
所以他可以只执行一次 , 适合做初始化操作, 或者其他一次性的操作, 不需要多次
sync.Map
提供的线程安全的map , 多线程访问时, 对于crud 操作, 会加锁.
maps := sync.Map{} // 存 maps.Store("","") // 删 maps.Delete("") // 取 maps.Load("") // 有就不存,返回已经存了的对象和true, 如果没有就返回存的value和false. maps.LoadOrStore("","") 复制代码
sync.Pool
顾名思义一个池子, 那么我们看看这个池子主要做啥了.
Pool用于存储那些被分配了但是没有被使用,而未来可能会使用的值,以减小垃圾回收的压力。(适合大对象)
同时他提供了一个存储的地方. 减少大量实例化过程 . 但是效率未必要比实例化快奥 . 因为维护一个对象要考虑各种问题, 这就是效率 , 但是实例化啥也不用考虑.
操作很简单.
func main() { pool := &sync.Pool{ New: func() interface{} { return "new" }, } // 首先的放一个 , 由于put操作. pool.Put("init") go func() { // 拿一个 s := pool.Get().(string) // 使用 fmt.Println(s) // 使用完放进去, 所以特别适合大对象. pool.Put(s) }() } 复制代码
sync.Cond
类似与Java的wait和notify 或者说 Condition ,更像后者,但是没有超时的机制
func main() { mutex := sync.Mutex{} start := sync.NewCond(&mutex) for x := 0; x < 10; x++ { go func() { start.L.Lock() defer start.L.Unlock() start.Wait() fmt.Println("do work") }() } go func() { time.Sleep(time.Second * 1) start.Broadcast() }() time.Sleep(time.Second * 2) } 复制代码
欢迎关注我们的微信公众号,每天学习Go知识

Recommend
-
59
相较于Go语言宣扬的“用通讯的方式共享数据”,通过共享数据的方式来传递信息和协调线程运行的做法其实更加主流,比较大多数的现代编程语言,都是用后一种方式作为并发编程的解决方案的。 一旦数据被多个线程共享,那么就很可能...
-
57
在 echo 官网的手册 上可以看到 echo 框架的路由性能主要依赖于...
-
41
今天在技术群中有小伙伴讨论并发安全的东西,其实之前就有写过map相关文章: 由浅入深聊聊Golang的map 。但是没...
-
43
前言 在 golang 中有一个池,它特别神奇,你只要和它有个约定,你要什么它就给什么,你用完了还可以还回去,但是下次拿的时候呢,确不一定是你上次存的那个,这个池就是 sync.Pool 说实话第一次看到这个东西的时候,...
-
11
一、前言 Go语言在设计上对同步(Synchronization,数据同步和线程同步)提供大量的支持,比如 goroutine和channel同步原语,库层面有 - sync:提供基本的同步原语(比如Mutex、RWMutex、Locker)和 工具类(Once、WaitGroup...
-
28
一、sync Map 包整体结构 本文主要阐述:Load、Store、De...
-
23
最近在工作中碰到了 GC 的问题:项目中大量重复地创建许多对象,造成 GC 的工作量巨大,CPU 频繁掉底。准备使用 sync.Pool 来缓存对象,减轻 GC 的消耗。为了用起来更顺畅,我特地研究了一番,形成此文。本文从使用到源码解...
-
49
sync包 sync是synchronization同步这个词的缩写,所以也会叫做同步包。这里提供了基本同步的操作,比如互斥锁等等。这里除了Once和WaitGroup类型之外,大多数类型都是供低级库例程使用的。更高级别的同步最好通过channel通道和co...
-
24
一、前言 Go语言在设计上对同步(Synchronization,数据同步和线程同步)提供大量的支持,比如 goroutine和channel同步原语,库层面有 - sync:提供基本的同步原语(比如Mutex、RWMutex、Locker)和 工具类(Once、WaitG...
-
16
踩了 Golang sync.Map 的一个坑 2020-08-24 2020-11-24go 1660 3.4k 6 分钟最近 Go 1.15 发布了,我也第一时间更新了这个版本,毕竟对 Go 的稳定性还...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK