sync.Map
go 1.9之后加入了线程安全的map,sync.Map。
源码中的注释为我们说明了sync.Map,是一个并发的map,恒定时间调用loads、stores、deletes。同时被多个goroutines调用是安全的。
在使用时不允许分享这些属性,它可能导致更坏的性能与更坏的安全性,相比较于使用一个普通的map与读写锁配合使用。
零map是有效的并且是空的。
在使用之后不允许被拷贝。
sync.Map的一些数据结构
type Map struct
type Map struct{
mu Mutex //锁
/*
一个只读的数据结构,可以在用不使用锁(mu)的情况下,并发安全的读。
自己读取失败也可以安全的加载,但是在存储时必须使用锁(mu)。
实际上也会更新这个数据的entries,如果entry是未删除的(unexpunged), 并不需要加锁。如果entry已经被删除了,需要加锁,以便更新dirty数据。
*/
read atomic.Value //只读
/*
dirty数据包含当前的map包含的entries,它包含最新的entries(包括read中未删除的数据,虽有冗余,但是提升dirty字段为read的时候非常快,不用一个一个的复制,而是直接将这个数据结构作为read字段的一部分),有些数据还可能没有移动到read字段中。
对于dirty的操作需要加锁,因为对它的操作可能会有读写竞争。
当dirty为空的时候, 比如初始化或者刚提升完,下一次的写操作会复制read字段中未删除的数据到这个数据中。
*/
dirty map[interface{}]*entry
/*
当从Map中读取entry的时候,如果read中不包含这个entry,会尝试从dirty中读取,这个时候会将misses加一,
当misses累积到 dirty的长度的时候, 就会将dirty提升为read,避免从dirty中miss太多次。因为操作dirty需要加锁。
*/
misses int
}
总体来说,它使用了冗余的数据结构read、dirty。dirty中会包含read中为删除的entries,新增加的entries会加入到dirty中。这应该是一种以空间换时间的方式。
readOnly
// 原子操作的修改Map.read
type readOnly struct {
m map[interface{}]*entry
amended bool // 如果Map.dirty有些数据不在中的时候,这个值为true
}
entry
type entry struct {
p unsafe.Pointer // *interface{}
}
p 指向interface{}存储entry的值,p 有三个值:
1. p == nil, entry已经被删除并且m.dirty == nil
2. p == expunged , entry已经被删除,并且m.dirty != nil,entry不存在与m.dirty中
3. 其他情况,entry是一个有效的值,记录在m.read.m[key], 如果m.dirty != nil,存在m.dirty[key]
sync.Map的导出方法
Load
加载存储在map中的值,如果没有值,则返回nil。OK的结果表示是否在map中找到值。
Load(key interface{}) (value interface{}, ok bool)
Load方法中有两个需要注意的地方:
1. 首先从m.read中加载,不存在的情况下,并且m.dirty中有新数据,加锁,然后从m.dirty中加载。
2.再一次读取read, _ = m.read.Load().(readOnly),虽然第一句执行的时候条件满足,但是在加锁之前,m.dirty可能被提升为m.read,所以加锁后还得再检查m.read,后续的方法中都使用了这个方法。
Store
存储一对键值
LoadOrStore
返回键的现有值(如果存在),否则存储并返回给定的值,如果是读取则返回true,如果是存储返回false。
Delete
删除一个键值
Range
循环读取map中的值。
示例
package main
import (
"fmt"
"sync"
)
type info struct {
Name string
Age int
}
var mp sync.Map
func main(){
mp.Store("Jame",28)
value,ok := mp.Load("Jame")
if !ok {
fmt.Println("there is no value")
}
fmt.Println(value) //输出value值 28
value,ok = mp.LoadOrStore(42,"Harden")
if !ok{
fmt.Println("1 set value ok ") // 输出
}else {
fmt.Println("1 value is ",value) // 未输出
}
value,ok = mp.LoadOrStore(42,"Tmac")
if !ok{
fmt.Println("2 set value ok ") // 未输出
}else {
fmt.Println("2 value is ",value) // 输出但value值为"Harden",证明若key存在会直接返回已存在的value
}
mp.Delete("Jame")
value,ok = mp.Load("Jame")
if !ok {
fmt.Println("there is no value, delete ok") //输出
}
mymap := make(map[string]info)
var user info
user.Name = "Tmac"
user.Age = 26
mymap["rocket"] = user
user.Name = "Kobe"
user.Age = 27
mymap["laker"] = user
mp.Store("NBA",mymap)
mapValue,ok := mp.Load("NBA")
if !ok{
fmt.Println("there is no value in NBA ")
}
for k,v := range mapValue.(interface{}).(map[string]info){
fmt.Println(k,v)
fmt.Println("name is ",v.Name," age is ",v.Age)
}
}
//go run 执行后输出:
28
1 set value ok
2 value is Harden
there is no value, delete ok
rocket {Tmac 26}
name is Tmac age is 26
laker {Kobe 27}
name is Kobe age is 27
Cond
Cond 实现一个条件变量,即等待或宣布事件发生的 goroutines 的会合点。
每一个Cond都会关联一个Locker L(通常是*Mutex或*RWMutex),当改变变量或者调用Wait方法时需要使用。
type Cond struct {
noCopy noCopy
// L is held while observing or changing the condition
L Locker
notify notifyList
checker copyChecker
Cond中的函数及导出方法
func NewCond(l Locker) *Cond
返回一个新的Cond与Locker l
Wait 等待通知
Wait原子的执行unlock c.L并延迟执行调用的goroutine。之后继续执行,Wait在返回之前锁住,Wait不能返回除非被Broadcast或者Signal唤醒。
Signal 单发通知
Signal唤醒一个goroutine等待Cond如果有。
Broadcast 广播通知
Broadcast唤醒所有的goroutine等待Cond
示例
package main
import (
"fmt"
"sync"
"time"
)
var l sync.Mutex
var cond = sync.NewCond(&l)
func message(im int){
cond.L.Lock() //wait 在返回之前锁住
cond.Wait() // 等待signal或Broadcast唤醒
fmt.Println("receive the message ",im)
time.Sleep(time.Second * 1)
cond.L.Unlock()
}
func main(){
for i := 0; i < 10; i ++{
go message(i)
}
fmt.Println("start message ...")
time.Sleep(time.Second * 1)
fmt.Println("send signal ... ")
cond.Signal() // 发送单个通知
time.Sleep(time.Second * 1)
fmt.Println("send signal again ... ")
cond.Signal() // 再次发送单个通知
time.Sleep(time.Second * 3)
fmt.Println("send broadcast ....")
cond.Broadcast() // 发送全部
time.Sleep(time.Second * 10)
}
// 输出
start message ...
send signal ...
receive the message 1
send signal again ...
receive the message 5
send broadcast ....
receive the message 6
receive the message 0
receive the message 8
receive the message 2
receive the message 9
receive the message 3
receive the message 7
receive the message 4
mutex与rwmutex
mutex中的注释有写到,提供基本的互斥锁。相对于Once与WaitGroup,更多的用于low-level library routines. Higher-level使用chanel通信更高。
值包含类别在这个包中定义的不能被拷贝。
mutex 互斥锁
Locker
整个包都围绕在这个interface,在上面我们也使用过了。
type Locker interface {
Lock()
Unlock()
}
Lock 加锁
如果这个锁已经被使用,调用它的goroutine将会block直到互斥锁可用(解锁)。
Unlock 解锁
如果没有被锁定调用Unlock解锁会报一个run-time的错误。
一个锁定的互斥说并不是与指定的goroutine关联的,它允许你使用一个goroutine锁定互斥锁,指定使用另一个goroutine去解锁它。
注:平时我们所说的锁定,其实就是去锁定互斥锁,而不是说去锁定一段代码。也就是说,当代码执行到有锁的地方时,它获取不到互斥锁的锁定,会阻塞在那里,从而达到控制同步的目的。
示例
package main
import (
"fmt"
"sync"
"time"
)
func main(){
var l sync.Mutex
ch := make(chan int ,10)
go func(){
l.Lock()
fmt.Println("Lock l .... ")
defer l.Unlock()
time.Sleep(time.Second * 1)
fmt.Println("Unlock l ...")
ch <- 1
}()
go func(){
fmt.Println("I wait for the mutex Unlock")
l.Lock()
fmt.Println("get Lock then I Lock l .... ")
defer l.Unlock()
time.Sleep(time.Second * 1)
fmt.Println("I Unlock it again ....")
ch <- 2
}()
for i := 0; i < 2;i ++{
fmt.Println("the ch is ",<-ch)
}
}
// 输出
Lock l ....
I wait for the mutex Unlock
Unlock l ...
get Lock then I Lock l ....
the ch is 1
I Unlock it again ....
the ch is 2
rwmutex 读写锁
它是针对读写操作的互斥锁,读写锁与互斥锁最大的不同就是可以分别对 读、写 进行锁定。一般用在大量读操作、少量写操作的情况。0值对于RWMutex是一个未锁定的互斥锁。
RWMutex再被使用之后不允许复制,不要混用锁定和解锁,如:Lock 和 RUnlock、RLock 和 Unlock。因为对未读锁定的读写锁进行读解锁或对未写锁定的读写锁进行写解锁将会引起运行时错误。
RLock 读锁定
你不能循环的去调用读锁定。被阻止的锁调用不允许新的读者获取锁
RUnlock 读解锁
对读锁定进行解锁。它不影响其他的读。如果对未锁定的调用Runlock会造成一个run-time错误。
Lock 写锁定
如果读或者写已经被锁定了,Lock会阻塞直到可用。
Unlock 写解锁
如果对未锁定的调用Unlock会造成一个run-time错误。与互斥锁相同可以一个goroutine锁定,另一个goroutine解锁。
注:读写锁
1. 同时只能有一个 goroutine 能够获得写锁定。
2. 同时可以有任意多个 gorouinte 获得读锁定。
3. 同时只能存在写锁定或读锁定(读和写互斥)。
也就是说,当有一个 goroutine 获得写锁定,其它无论是读锁定还是写锁定都将阻塞直到写解锁;当有一个 goroutine 获得读锁定,其它读锁定任然可以继续;当有一个或任意多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定。所以说这里的读锁定(RLock)目的其实是告诉写锁定:有很多人正在读取数据,你给我站一边去,等它们读(读解锁)完你再来写(写锁定)。
示例
package main
import (
"fmt"
"sync"
"time"
)
var rw sync.RWMutex
func read(num int){
fmt.Println("start read ... ")
rw.RLock()
fmt.Println("read lock reading ... ",num )
time.Sleep(time.Second * 1)
rw.RUnlock()
fmt.Println("read end unlock .... ",num )
}
func write(num int){
fmt.Println("start writing ... ")
rw.Lock()
fmt.Println("writing lock ... ",num )
time.Sleep(time.Second * 1)
rw.Unlock()
fmt.Println("writing unlock ... ",num)
}
func main(){
go func(){
for i := 0; i < 2; i++{
read(i)
}
}()
go func(){
for i := 10; i < 12;i ++{
write(i)
}
}()
time.Sleep(time.Second * 10)
}
//输出
start writing ...
start read ...
writing lock ... 10
writing unlock ... 10
start writing ...
read lock reading ... 0
read end unlock .... 0
start read ...
writing lock ... 11
writing unlock ... 11
read lock reading ... 1
read end unlock .... 1
从示例中我们可以看到我开启了两个goroutine循环的调用read与write,当写锁定时不可以读,当读锁定时不可以写
WaitGroup
WaitGroup 用于等待一组 goroutine 结束
WaitGroup中的方法
Add
Add 用来添加 goroutine 的个数
Done
Done 执行一次数量减 1
Wait
Wait 用来等待结束
示例
package main
import(
"fmt"
"time"
"sync"
)
func main(){
var wg sync.WaitGroup
for i := 0;i < 10;i++{ //累计计数,注意go中只支持后加加
wg.Add(1)
go func(id int){
defer wg.Done() //递减计数
time.Sleep(time.Second)
fmt.Println("go ",id,"done")
}(i)
}
fmt.Println("main:...")
wg.Wait() //阻塞直至计数归零
fmt.Println("main exit")
}
//输出
main:…
go 2 done
go 3 done
go 5 done
go 8 done
go 4 done
go 7 done
go 1 done
go 6 done
go 9 done
go 0 done
main exit
注: 尽管WaitGroup.Add实现了原子操作,但建议在goroutine外累加计数器,以免Add尚未执行,wait以及退出。可在多处使用wait阻塞,它们都能接收到通知
package main
import(
"fmt"
"time"
"sync"
)
func main(){
var wg sync.WaitGroup
wg.Add(1)
go func(){
wg.Wait()
fmt.Println("wait exit!")
}()
go func(){
time.Sleep(time.Second)
fmt.Println("done")
wg.Done()
}()
wg.Wait()
fmt.Println("main exit")
}
//输出
done
wait exit!
main exit
Pool 临时对象池
可以作为临时对象的保存和复用的集合。是goroutines 协程安全,可以同时被多个协程使用。
pool在使用之后不允许拷贝。新键 Pool 需要提供一个 New 方法,目的是当获取不到临时对象时自动创建一个(不会主动加入到 Pool 中)。
Pool 实际上会为每一个操作它的 goroutine 相关联的 P 都生成一个本地池。如果从本地池 Get 对象的时候,本地池没有,则会从其它的 P 本地池获取。因此,Pool 的一个特点就是:可以把由其中的对象值产生的存储压力进行分摊。
它有着以下特点:
Pool 中的对象在仅有 Pool 有着唯一索引的情况下可能会被自动删除(取决于下一次 GC 执行的时间)。
Pool中的导出方法
Put 放置
Get 获取
官方示例
package main
import (
"bytes"
"io"
"os"
"sync"
"time"
)
var bufPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func timeNow() time.Time {
return time.Unix(1136214245, 0)
}
func Log(w io.Writer, key, val string) {
// 获取临时对象,没有的话会自动创建
b := bufPool.Get().(*bytes.Buffer)
b.Reset()
b.WriteString(timeNow().UTC().Format(time.RFC3339))
b.WriteByte(' ')
b.WriteString(key)
b.WriteByte('=')
b.WriteString(val)
w.Write(b.Bytes())
// 将临时对象放回到 Pool 中
bufPool.Put(b)
}
func main() {
Log(os.Stdout, "path", "/search?q=flowers")
}
打印结果:
2006-01-02T15:04:05Z path=/search?q=flowers
注: Pool适用与无状态的对象的复用,而不适用与如连接池之类的
Once
可以使得函数多次调用只执行一次。
Once中的导出方法
Do 执行
只有一个方法,如果do被同时调用,只有第一次会被执行。
示例
package main
import (
"fmt"
"sync"
"time"
)
func action(){
fmt.Println("Only once ",)
}
func main(){
var once sync.Once
for i := 0;i < 10;i++{
go func(i int ){
once.Do(action)
fmt.Println("---- ",i)
}(i)
}
time.Sleep(time.Second * 5)
}
//输出
Only once
---- 1
---- 2
---- 0
---- 6
---- 8
---- 3
---- 4
---- 7