078-只被执行一次的函数

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/q1007729991/article/details/82026278

倘若你看过我的所有文章的话,你肯定还记得曾经在学习《Linux 环境编程》的时候,也有一篇类似的文章,标题和这个一模一样——《只被执行一次的函数》

我强烈建议你回顾一下上面那篇文章。如果你没有《Linux 环境编程》的基础,强烈建议你补习一下,只会 Golang 是不行的。

1. 背景

相信很多同学都使用过单例模式。如果在单线程程序中,单例模式肯定没啥问题,但是如果在多线程程序中,可能就容易出问题了。当然,很多同学想到的方法是加锁来解决,像下面这样:

T* getInstance() {
    mutex.lock()
    if (_instance == nullptr) {
        _instance = new T();
    }
    mutex.unlock()

    return _instance;
}

这个加锁粒度太大了,而且每次访问都需要加锁。也有同学会说,使用 static 全局初始化不就好了(有人叫它为饿汉模式)。没错,这是一种可行的方法,但是缺点是有时候可能一次也用不到这个单例,一上来就初始化有点太浪费。

那使用 double-check 方法改一下(所谓的线各安全的懒汉模式)?

T* getInstance() {
    if (_instance == nullptr) {
        mutex.lock()
        if (_instance == nullptr) {
            _instance = new T();
        }
        mutex.unlock()
    }
    return _instance;
}

很不幸运的是,即使是 double-check,上面的代码在多线程环境中还是会出问题。比如线程 1 执行 _instance = new T(),_instance 已经不为 nullptr 了,但是实际上 T 对象还没有构造出来,即构造函数还未执行。(对象构造必须是先申请内存,再执行构造函数)。这个时候线程 2 在第一个 if 条件看到的 _instance 不为 nullptr,于是返回……

看起来好像挺麻烦的。不用怕,我们有杀器,可以更加简单且完美的解决这个问题——那就是 sync.Once 类型。

2. 简单的 demo

为了让你快速体验 sync.Once 的作用,来看下面一段代码。

package main

import (
    "fmt"
    "sync"
)

func hello() {
    fmt.Println("only once")
}

func main() {
    var once sync.Once
    once.Do(hello) // 把 hello 函数托管给 once 对象去执行。
    fmt.Println("first")
    once.Do(hello) // 第二次托管,once 就不理我们了。。。
    fmt.Println("second")
}

// Output:
// only once
// first
// second

其实,once.Do 也是“协程”安全的,你甚至可以这样:

func main() {
    var once sync.Once
    var wg sync.WaitGroup
    wg.Add(10)
    // 10 个协程同时托管 hello 函数给 once
    for i := 0; i < 10; i++ {
        go func() {
            once.Do(hello)
            wg.Done()
        }()
    }
    wg.Wait()
    fmt.Println("first")
    fmt.Println("second")
}

// Output:
// only once
// first
// second

3. 使用 once 来实现懒汉单例

package main

import (
    "fmt"
    "sync"
)

type Configure struct{
    volume int32
}

var once sync.Once
var configure *Configure

func initConfig() {
    configure = &Configure{10}
}

func GetInstance() *Configure {
    // initConfig()
    once.Do(initConfig) // 注释这一行,打开上一行试试
    return configure
}

func main() {
    var conf *Configure
    fmt.Printf("%p\n", conf)

    var wg sync.WaitGroup

    wg.Add(10)
    for i := 0; i < 10; i++ {
        go func() {
            conf = GetInstance()
            fmt.Printf("%p\n", conf)
            wg.Done()
        }()
    }
    wg.Wait()
}

4. 总结

  • 掌握 sync.Once 的作用和使用方法

猜你喜欢

转载自blog.csdn.net/q1007729991/article/details/82026278