前面刚讲到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 获得读锁定,其它读锁定仍然可以继续 ;

当有一个或任意多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定 。

总结上面的三句话可以得出结论:

  1. 同时只能有一个 goroutine 能够获得写锁定;
  2. 同时可以有任意多个 goroutine 获得读锁定;
  3. 同时只能存在写锁定或读锁定(读和写互斥)。

看一个读写锁的例子:

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包学习的更多相关文章

  1. golang 中 sync包的 WaitGroup

    golang 中的 sync 包有一个很有用的功能,就是 WaitGroup 先说说 WaitGroup 的用途:它能够一直等到所有的 goroutine 执行完成,并且阻塞主线程的执行,直到所有的 ...

  2. Java中的包学习笔记

    一.总结 1.引入包的概念的原因和包的作用比如有多个人开发一个大型程序,A定义了一个Math.java类,B也定义了一个Math.java类,它们放在不同目录,使用的时候也是用目录来区分,包实际上就是 ...

  3. golang中context包学习

    摘要 go语言中goroutine之间的关联关系,缺乏维护,在erlang中有专门的机制来保障新开仟程的生命周期, 在go语言中,只能通过channel + select来实现,但不够直观,感觉很绕. ...

  4. golang中sync.RWMutex和sync.Mutex区别

    golang中sync包实现了两种锁Mutex (互斥锁)和RWMutex(读写锁),其中RWMutex是基于Mutex实现的,只读锁的实现使用类似引用计数器的功能. type Mutex     f ...

  5. R语言爬虫初尝试-基于RVEST包学习

    注意:这文章是2月份写的,拉勾网早改版了,代码已经失效了,大家意思意思就好,主要看代码的使用方法吧.. 最近一直在用且有维护的另一个爬虫是KINDLE 特价书爬虫,blog地址见此: http://w ...

  6. Android IOS WebRTC 音视频开发总结(八十七)-- WebRTC中丢包重传NACK实现分析

    本文主要介绍WebRTC中丢包重传NACK的实现,作者:weizhenwei ,文章最早发表在编风网,微信ID:befoio 支持原创,转载必须注明出处,欢迎关注我的微信公众号blacker(微信ID ...

  7. PHP中的Libevent学习

    wangbin@2012,1,3 目录 Libevent在php中的应用学习 1.      Libevent介绍 2.      为什么要学习libevent 3.      Php libeven ...

  8. go语言中sync包和channel机制

    文章转载至:https://www.bytelang.com/article/content/A4jMIFmobcA= golang中实现并发非常简单,只需在需要并发的函数前面添加关键字"Go&quo ...

  9. .NET Core中的包、元包与框架

    本文为翻译文章,原文:Packages, Metapackages and Frameworks .NET Core是一个由NuGet包组成的平台.一些产品受益于细粒度包的定义,也有一些受益于粗粒度包 ...

随机推荐

  1. 如何使用 Docker 安装 Jenkins

    说在前面 本篇内容非常简单,仅讲述了如何快速在 Docker 上部署一个 Jenkins 实例,不涉及其他. 本文实验环境: 操作系统:Centos 7.5 Docker Version:18.09. ...

  2. TCP中的粘包问题,以及用TCP和UDP实现多次聊天

    TCP协议 在连接内多和客户端说几句 #server端 import socket sk = socket.socket() sk.bind(('127.0.0.1',9001)) sk.listen ...

  3. vue中局部组件的使用

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  4. [AI开发]目标跟踪之行为分析

    基于视频结构化的应用中,目标在经过跟踪算法后,会得到一个唯一标识和它对应的运动轨迹,利用这两个数据我们可以做一些后续工作:测速(交通类应用场景).计数(交通类应用场景.安防类应用场景)以及行为检测(交 ...

  5. F#周报2019年第28期

    新闻 FableConf门票开始贩售 Bolero的HTML模板支持热加载 Bolero从v0.4到v0.5的升级指南 完整的SAFE-Chat迁移至了Fable 2 为纯函数式3D图形生成领域专用语 ...

  6. WebApi 通过拦截器设置特定的返回格式

    public class ActionFilter : ActionFilterAttribute { /// <summary> /// Action执行之后由MVC框架调用 /// & ...

  7. [译]Vulkan教程(33)多重采样

    [译]Vulkan教程(33)多重采样 Multisampling 多重采样 Introduction 入门 Our program can now load multiple levels of d ...

  8. web前端笔试篇(一)

    [ 题外话 ]:本博主作为一名准毕业生,即将面临毕业就业问题,即将到大四了,不准备考研的我,那么该去干嘛呢?毫无疑问,那就是实习,那么即使是实习,那么在要想进入自己心仪的企业之前,笔试这一关终究是无法 ...

  9. 洛谷 P1970 花匠

    题目描述 花匠栋栋种了一排花,每株花都有自己的高度.花儿越长越大,也越来越挤.栋栋决定把这排中的一部分花移走,将剩下的留在原地,使得剩下的花能有空间长大,同时,栋栋希望剩下的花排列得比较别致. 具体而 ...

  10. 艺赛旗RPA-处理无表头表格

    今天写一个demo,要求是对表格数据用价格为key进行排序 样本数据有两种格式: 一.第一行是一个大单元格 处理步骤: 在不变参数的情况下读取表格数据: 结果如下: 可以看见表头: Unnamed: ...