初始化单例的示例
在平时的开发里,如果初始化单例资源,比如定义包级别的变量,通常会这样写:
1
2
3
4
5
|
package demo
import "time"
var StartTime = time.Now()
|
或者在init()
函数进行初始化:
1
2
3
4
5
6
7
8
9
10
|
package demo
import "time"
var StartTime time.Time
func init() {
StartTime = time.Now()
}
|
又或者在main函数开始执行的时候进行初始化。这三种方式都是并发安全的。
但很多时候我们要延迟惊醒初始化的时候,会这样写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
package main
import (
"net"
"sync"
"time"
)
// 使用互斥锁保证线程(goroutine)安全
var connMu sync.Mutex
var conn net.Conn
func getConn() net.Conn {
connMu.Lock()
defer connMu.Unlock()
// 返回已创建好的连接
if conn != nil {
return conn
}
// 创建连接
conn, _ = net.DialTimeout("tcp", "baidu.com:80", 10*time.Second)
return conn
}
// 使用连接
func main() {
conn := getConn()
if conn == nil {
panic("conn is nil")
}
}
|
这种方式实现起来很简单,但是依然有一点性能问题,每次每次请求的时候还是得竞争锁才能读到conn
,这是比较浪费资源的,因为conn
如果创建好之后,其实就不需要锁的保护了。
针对这种场景,可以使用sync.Once
这个并发原语。
Once的用法
sync.Once
与init
的区别:
- init函数是在包首次被加载的时候执行,只执行一次。
- sync.Once是在代码运行中需要的时候执行,只执行一次。
1
|
func (o *Once) Do(f func())
|
sync.Once
只有一个Do()
方法,Do()
方法可以被多次调用,但只有第一次调用Do()
方法时,f参数才会执行。(f必须是无参数无返回值函数)。
因为f参数是一个无参数无返回值的函数,所以可以通过闭包的方式引用外部的变量:
1
2
3
4
5
6
7
8
9
|
var addr = "www.google.com"
var (
conn net.Conn
err error
)
once.DO(func() {
conn, err = net.Dial("tcp", addr)
})
|
在绝大多数的情况下都会使用闭包的方式去初始化一个外部的资源。
Once的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
type Once struct {
done uint32
m Mutex
}
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 {
// Outlined slow-path to allow inlining of the fast-path.
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
|
首先通过原子操作读取done
字段,如果没有改变则执行doSlow()方法。
doSlow()
方法里加入了互斥锁,保证只有一个goroutine
进行初始化,同时利用双检查的机制,再次判断done
是否为0,如果为0,则是第一次执行,执行完毕后,通过原子操作将done
设置为1,然后释放锁。
因为使用的是双检查机制,即使此时有多个goroutine
进入了doSlow()
方法,后续的goroutine
会看到done
的值为·,也不会再次执行f()。这样既保证了并发的goroutine
会等待f()完成,而且还不会多次执行f()。
使用Once可能出现的错误
deadlock
在f()
参数中再次调用当前的这个Once
的Do()
的话,会导致死锁。
未初始化
如果f()
方法执行的时候发生panic
,或者f()
执行初始化资源的时候失败了,这个时候,Once
还是会认为初次执行已经成功了,即使再次调用Do()
方法,也不会再次执行f()
。