Go中sync包学习
前面刚讲到goroutine和channel,通过goroutine启动一个协程,通过channel的方式在多个goroutine中传递消息来保证并发安全。今天我们来学习sync包,这个包是Go提供的基础包,提供了锁的支持。但是Go官方给的建议是:不要以共享内存的方式来通信,而是要以通信的手段来共享内存。所以他们是提倡使用channel的方式来实现并发控制。
学过Java的同学对锁的概念肯定不陌生,在Java中提供Sychronized
关键字提供独占锁,Lock
类提供读写锁。在sync包中实现的功能也是与锁相关,包中主要包含的对象有:
- Locker:提供了加锁和解锁的接口
- Cond:条件等待通过 Wait 让例程等待,通过 Signal 让一个等待的例程继续,通过 Broadcast 让所有等待的例程继续。
- Map:线程安全的map ,同时被多个goroutines调用是安全的。
- Mutex:互斥锁,用来保证在任一时刻,只能有一个例程访问某对象。实现了Locker接口。Mutex 的初始值为解锁状态,Mutex 通常作为其它结构体的匿名字段使用,使该结构体具有 Lock 和 Unlock 方法
- Once:Once 是一个可以被多次调用但是只执行一次,若每次调用Do时传入参数f不同,但是只有第一个才会被执行。
- Pool:用于存储临时对象,它将使用完毕的对象存入对象池中,在需要的时候取出来重复使用,其中存放的临时对象随时可能被 GC 回收掉如果该对象不再被其它变量引用
- RWMutex:读写互斥锁,RWMutex 比 Mutex 多了一个“读锁定”和“读解锁”,可以让多个例程同时读取某对象。RWMutex 的初始值为解锁状态。RWMutex 通常作为其它结构体的匿名字段使用。
- WaitGroup :用于等待一组例程的结束。主例程在创建每个子例程的时候先调用 Add 增加等待计数,每个子例程在结束时调用 Done 减少例程计数。之后主例程通过 Wait 方法开始等待,直到计数器归零才继续执行。
1. Mutex 互斥锁使用
我们先用Go写一段经典的并发场景:
package main
import (
"fmt"
"time"
)
func main() {
var a = 0
for i := 0;i<1000;i++{
go func(i int) {
a += 1
fmt.Println(a)
}(i)
}
time.Sleep(time.Second)
}
运行这段程序,你会发现最后输出的不是1000。
这个时候你可以使用Mutex:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var a = 0
var lock sync.Mutex
for i := 0;i<1000;i++{
go func(i int) {
lock.Lock()
a += 1
fmt.Println(a)
lock.Unlock()
}(i)
}
time.Sleep(time.Second)
}
Mutex实现了Locker接口,所以他有Lock()方法和Unlock()方法。只需要在需要同步的代码块上下使用这两个方法就好。
Mutex等同于Java中的Synchronized关键字或者Lock。
2. 读写锁-RWMutex
类似于Java中的ReadWriteLock。读写锁有如下四个方法:
写操作的锁定和解锁
* func (*RWMutex) Lock
* func (*RWMutex) Unlock
读操作的锁定和解锁
* func (*RWMutex) Rlock
* func (*RWMutex) RUnlock
当有一个 goroutine 获得写锁定,其它无论是读锁定还是写锁定都将阻塞直到写解锁;
当有一个 goroutine 获得读锁定,其它读锁定仍然可以继续 ;
当有一个或任意多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定 。
总结上面的三句话可以得出结论:
- 同时只能有一个 goroutine 能够获得写锁定;
- 同时可以有任意多个 goroutine 获得读锁定;
- 同时只能存在写锁定或读锁定(读和写互斥)。
看一个读写锁的例子:
package main
import (
"fmt"
"strconv"
"sync"
"time"
)
var (
rwLock sync.RWMutex
data = ""
)
func read(ran int) {
time.Sleep(time.Duration(ran) * time.Microsecond)
rwLock.RLock()
fmt.Printf("读操作开始:%s\n",data)
data = ""
rwLock.RUnlock()
}
func write(subData string) {
rwLock.Lock()
data = subData
fmt.Printf("写操作开始:%s\n",data)
rwLock.Unlock()
}
func deduce() {
for i:=0;i<10;i++ {
go write(strconv.Itoa(i))
}
for i:=0;i<10;i++ {
go read(i * 100)
}
}
func main() {
deduce()
time.Sleep(2*time.Second)
}
运行上面的程序,会发现写操作都执行了,但是读操作不是将所有写的数字都读出来了。这是因为读操作是可以同时有多个goroutine获取锁的,但是写操作只能同时有一个goroutine执行。
3. WaitGroup
WaitGroup 用于等待一组 goroutine 结束,它有三个方法:
func (wg *WaitGroup) Add(delta int)
func (wg *WaitGroup) Done()
func (wg *WaitGroup) Wait()
与Java中类比的话,相似与CountDownLatch。
package main
import (
"fmt"
"sync"
"time"
)
func goWithMountain(p int,wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("%d,我已经上来了\n",p)
}
func main() {
var wg sync.WaitGroup
wg.Add(10)
for i:=0;i<10;i++ {
go goWithMountain(i,&wg)
}
wg.Wait()
time.Sleep(2*time.Second)
fmt.Printf("=登山结束\n")
}
输出:
0,我已经上来了
9,我已经上来了
3,我已经上来了
7,我已经上来了
8,我已经上来了
6,我已经上来了
2,我已经上来了
4,我已经上来了
5,我已经上来了
1,我已经上来了
=登山结束
是不是有一样一样的呢。
4. Cond条件变量
与互斥量不同,条件变量的作用并不是保证在同一时刻仅有一个线程访问某一个共享数据,而是在对应的共享数据的状态发生变化时,通知其他因此而被阻塞的线程。条件变量总是与互斥量组合使用。互斥量为共享数据的访问提供互斥支持,而条件变量可以就共享数据的状态的变化向相关线程发出通知。 下面给出主要的几个函数:
func NewCond(l Locker) *Cond:用于创建条件,根据实际情况传入sync.Mutex或者sync.RWMutex的指针,一定要是指针,否则会发生复制导致锁的失效
func (c *Cond) Broadcast():唤醒条件上的所有goroutine
func (c *Cond) Signal():随机唤醒等待队列上的goroutine,随机的方式效率更高
func (c *Cond) Wait():挂起goroutine的操作
看一个读写操作的例子:
package main
import (
"bytes"
"fmt"
"io"
"sync"
"time"
)
type MyDataBucket struct {
br *bytes.Buffer
gmutex *sync.RWMutex
rcond *sync.Cond //读操作需要用到的条件变量
}
func NewDataBucket() *MyDataBucket {
buf := make([]byte, 0)
db := &MyDataBucket{
br: bytes.NewBuffer(buf),
gmutex: new(sync.RWMutex),
}
db.rcond = sync.NewCond(db.gmutex.RLocker())
return db
}
func (db *MyDataBucket) Read(i int) {
db.gmutex.RLock()
defer db.gmutex.RUnlock()
var data []byte
var d byte
var err error
for {
//读取一个字节
if d, err = db.br.ReadByte(); err != nil {
if err == io.EOF {
if string(data) != "" {
fmt.Printf("reader-%d: %s\n", i, data)
}
db.rcond.Wait()
data = data[:0]
continue
}
}
data = append(data, d)
}
}
func (db *MyDataBucket) Put(d []byte) (int, error) {
db.gmutex.Lock()
defer db.gmutex.Unlock()
//写入一个数据块
n, err := db.br.Write(d)
db.rcond.Broadcast()
return n, err
}
func main() {
db := NewDataBucket()
go db.Read(1)
go db.Read(2)
for i := 0; i < 10; i++ {
go func(i int) {
d := fmt.Sprintf("data-%d", i)
db.Put([]byte(d))
}(i)
time.Sleep(100 * time.Millisecond)
}
}
上例中,读操作必依赖于写操作先写入数据才能开始读。当读取的数据为空的时候,会先调用wait()方法阻塞当前方法,在Put方法中写完数据之后会调用Broadcast()去广播,告诉阻塞者可以开始了。
5.Pool 临时对象池
Pool 用于存储临时对象,它将使用完毕的对象存入对象池中,在需要的时候取出来重复使用,目的是为了避免重复创建相同的对象造成 GC 负担过重。从 Pool 中取出对象时,如果 Pool 中没有对象,将返回 nil,但是如果给 Pool.New 字段指定了一个函数的话,Pool 将使用该函数创建一个新对象返回。
sync.Pool可以安全被多个线程同时使用,保证线程安全。这个Pool和我们一般意义上的Pool不太一样 ,Pool无法设置大小,所以理论上只受限于系统内存大小。Pool中的对象不支持自定义过期时间及策略,究其原因,Pool并不是一个Cache。
看一个小例子:
package main
import (
"fmt"
"sync"
)
func main() {
//我们创建一个Pool,并实现New()函数
sp := sync.Pool{
New: func() interface{} {
return make([]int, 16)
},
}
item := sp.Get()
fmt.Println("item : ", item)
//我们对item进行操作
//New()返回的是interface{},我们需要通过类型断言来转换
for i := 0; i < len(item.([]int)); i++ {
item.([]int)[i] = i
}
fmt.Println("item : ", item)
//使用完后,我们把item放回池中,让对象可以重用
sp.Put(item)
//再次从池中获取对象
item2 := sp.Get()
//注意这里获取的对象就是上面我们放回池中的对象
fmt.Println("item2 : ", item2)
//我们再次获取对象
item3 := sp.Get()
//因为池中的对象已经没有了,所以又重新通过New()创建一个新对象,放入池中,然后返回
//所以item3是大小为16的空[]int
fmt.Println("item3 : ", item3)
}
输出:
item : [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
item : [0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15]
item2 : [0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15]
item3 : [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
6. Once 执行一次
Once 的作用是多次调用但只执行一次,Once 只有一个方法,Once.Do(),向 Do 传入一个函数,这个函数在第一次执行 Once.Do() 的时候会被调用,以后再执行 Once.Do() 将没有任何动作,即使传入了其它的函数,也不会被执行,如果要执行其它函数,需要重新创建一个 Once 对象。
看一个很简单的例子:
package main
import (
"fmt"
"sync"
)
func main() {
var once sync.Once
onceBody := func() {
fmt.Println("我只会出现一次")
}
done := make(chan bool)
for i := 0; i < 3; i++ {
go func() {
once.Do(onceBody)
done <- true
}()
}
for i := 0; i < 3; i++ {
<-done
}
}
Go中sync包学习的更多相关文章
- golang 中 sync包的 WaitGroup
golang 中的 sync 包有一个很有用的功能,就是 WaitGroup 先说说 WaitGroup 的用途:它能够一直等到所有的 goroutine 执行完成,并且阻塞主线程的执行,直到所有的 ...
- Java中的包学习笔记
一.总结 1.引入包的概念的原因和包的作用比如有多个人开发一个大型程序,A定义了一个Math.java类,B也定义了一个Math.java类,它们放在不同目录,使用的时候也是用目录来区分,包实际上就是 ...
- golang中context包学习
摘要 go语言中goroutine之间的关联关系,缺乏维护,在erlang中有专门的机制来保障新开仟程的生命周期, 在go语言中,只能通过channel + select来实现,但不够直观,感觉很绕. ...
- golang中sync.RWMutex和sync.Mutex区别
golang中sync包实现了两种锁Mutex (互斥锁)和RWMutex(读写锁),其中RWMutex是基于Mutex实现的,只读锁的实现使用类似引用计数器的功能. type Mutex f ...
- R语言爬虫初尝试-基于RVEST包学习
注意:这文章是2月份写的,拉勾网早改版了,代码已经失效了,大家意思意思就好,主要看代码的使用方法吧.. 最近一直在用且有维护的另一个爬虫是KINDLE 特价书爬虫,blog地址见此: http://w ...
- Android IOS WebRTC 音视频开发总结(八十七)-- WebRTC中丢包重传NACK实现分析
本文主要介绍WebRTC中丢包重传NACK的实现,作者:weizhenwei ,文章最早发表在编风网,微信ID:befoio 支持原创,转载必须注明出处,欢迎关注我的微信公众号blacker(微信ID ...
- PHP中的Libevent学习
wangbin@2012,1,3 目录 Libevent在php中的应用学习 1. Libevent介绍 2. 为什么要学习libevent 3. Php libeven ...
- go语言中sync包和channel机制
文章转载至:https://www.bytelang.com/article/content/A4jMIFmobcA= golang中实现并发非常简单,只需在需要并发的函数前面添加关键字"Go&quo ...
- .NET Core中的包、元包与框架
本文为翻译文章,原文:Packages, Metapackages and Frameworks .NET Core是一个由NuGet包组成的平台.一些产品受益于细粒度包的定义,也有一些受益于粗粒度包 ...
随机推荐
- MySQL错误:The user specified as a definer (XXX@XXX) does not exist (1449错误)最简解决方案
背景:从同事处通过备份和还原备份方法导入mysql数据库,导入成功后启动项目,发现出现以下错误:The user specified as a definer (XXX@XXX) does not e ...
- DStream转为DF的两种方式(突破map时元组22的限制)
在进行Spark Streaming的开发时,我们常常需要将DStream转为DataFrame来进行进一步的处理, 共有两种方式,方式一: val spark = SparkSession.buil ...
- IDEA为新手专业打造
IDEA为新手专业打造 一.创建一个项目 新手的话可以先创建一个空项目 项目创建完成后会弹出一个Project Settings设置框,点击Project进行如图设置,设置完成点击OK 一.在创建的项 ...
- Java编程思想:泛型接口
import java.util.Iterator; import java.util.Random; public class Test { public static void main(Stri ...
- idea的安装与配置及基本用法
Intellij IDEA 确实使用更加方便,由于目前只用到maven项目,所以此处只记录maven项目的配置. 一.配置idea前准备: 1.下载idea安装包.jdk安装包.maven安装包.gi ...
- Excel催化剂开源第48波-Excel与PowerBIDeskTop互通互联之第二篇
前一篇的分享中,主要谈到Excel透视表连接PowerBIDeskTop的技术,在访问SSAS模型时,不止可以使用透视表的方式访问,更可以发数据模型发起DAX或MDX查询,返回一个结果表数据,较透视表 ...
- 个人永久性免费-Excel催化剂功能第48波-拆分工作薄内工作表,堪称Excel界的单反
一个工作薄有多个相同类型的工作表,然后想通过批量操作,把每个工作表都另存为一个工作薄文件,这个批量拆分工作薄,绝大多数插件都有此功能,就如懂点VBA的高级用户也常常有点不屑于用插件来完成,自己写向行V ...
- java 金额的大小写转换类
/** *金额大小写转换工具类 */ public class MoneyUtil { /** 大写数字 */ private static final String[] NUMBERS = { &q ...
- 解读Android MediaPlayer 详细使用方法
MediaPlayer具有非常强大的功能,对音视频的播放均提供了支持,为了保证播放期间系统的正常工作,需要设置"android.permission.WAKE_LOCK"权 ...
- 如何实现Kali linux系统下的U盘启动(小白指导)
一.准备工作: 声明:这个“操作”并不会影响你原装的系统,真正的即插即用的哦. (1)4GB的U盘<读写速度比较快的> (2)Kali linux镜像文件 (3)软件Universal-U ...