6

基于CAS实现线程安全的单例模式

 2 years ago
source link: https://allenwind.github.io/blog/4192/
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.
neoserver,ios ssh client
Mr.Feng Blog

NLP、深度学习、机器学习、Python、Go

基于CAS实现线程安全的单例模式

之前写到基于CAS实现的锁,也写过各类语言实现单例模式,其中包括线程安全的实现和线程不安全的实现。线程安全的实现大致思路都是加锁,其中性能较好的是double-check的加锁方法。另外对于Java可以使用synchronized实现封锁,它本质上也是使用了锁;还有一类方式是使用静态内部类,由于类的加载机制,这样实现的单例模式也是线程安全的。类的加载机制也是使用了锁,哈哈,又绕回来。今天聊聊不用锁实现线程安全的单例模式。使用Go语言。

基于CAS实现的锁一文中,提到过通过原子操作实现锁,其原理大致就是通过变量标记锁的状态,线程在获取锁是原子地通过CAS操作修改锁的状态,一旦修改成功,锁获取成功,锁的释放也类似。这就是基于CAS实现锁的核心。

既然锁的实现是基于CAS,而线程安全的单例模式的实现需要锁,于是干脆绕过封锁过程,直接实现CAS实现单例模式。通过一个变量标记某类是否创建了实例,例如0表示没有创建,1表示已经创建。而该变量的修改是基于CAS原子地进行,这样,当多个线程尝试把该变量从0修改为1时,只有一个线程操作成功。于是确保某类只能创建一个实例。

使用代码表达如下,我们以创建日志记录对象为例:

package main

import (
"fmt"
"sync"
"sync/atomic"
)

type Logger struct {
// something about logging
}

var exists int32 = 0
var logger *Logger

func NewLogger() *Logger {
if atomic.CompareAndSwapInt32(&exists, 0, 1) {
logger = &Logger{}
return logger
} else {
return logger
}
}

简单验证它是否线程安全

func test() {

var wg sync.WaitGroup

wg.Add(1000)

channel := make(chan *Logger, 1000)
for i := 0; i < 1000; i++ {
go func() {
channel <- NewLogger()
wg.Done()
}()

}

wg.Wait()

for i := 0; i < 1000; i++ {
v := <-channel
if v != logger {
panic("线程不安全")

}
}

fmt.Println("所有goroutine创建的实例唯一")
}

当把exists变量从0改为1后(没有操作把它改回去),再也没有线程创建新的logger,确保全局只有一个logger。如果我们能保证创建过程在程序运行期间只运行一次(无论是单线程还是多线程下)那么,程序的生命周期中只有一个实例对象。使用sync.Once对象满足这个过程。

package main

import (
"fmt"
"sync"
)

type Logger struct {
// something about logging
}

var once sync.Once
var logger *Logger

func NewLogger() *Logger {
once.Do(func() {
logger = new(Logger)
// init logger here
})
return logger
}

func main() {
logger1 := NewLogger()
logger2 := NewLogger()

fmt.Println(logger1 == logger2)
}

当然,从源码来看

type Once struct {
m Mutex
done uint32
}

func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 1 {
return
}
// Slow-path.
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}

这种方法使用了锁。这里通过锁确保任何时候只有单个线程进入临界区,也就是只有一个线程在执行函数f,同时原子性地标记f被执行过。

对比上述两种方法,方法二更简洁。简洁。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK