在前面章节 golang 并发–goroutine(四)我们讲过 golang 是天生支持高并发的语言。那么有并发必然涉及到线程安全的问题,为了防止多个 go 程同时操作同一个临界资源,我们必然需要引入锁,golang 内置库 sync 就是为我们提供锁的操作方法的。这篇文章我们通过一些例子来看一看 sync 库怎么用。

golang 单例模式

单例模式是常见的一种设计模式,学习过 java 的伙伴们应该知道单例模式可以保障在一个程序中一个类只实例化一个对象。在 golang 中没有类和对象的概念,但是存在类似的概念 struct (结构体),golang 单的例模式的就是保证一个结构体只能实例化出一个变量。

下面我们就先通过实现一个 golang 单例模式来学习 sync 库的用法。

非线程安全的单例模式

package main

import (
    "fmt"
    "sync"
    "time"
)

type testStruct struct {
    num int
}

var mu sync.Mutex
var instance *testStruct
var num int = 0

func NewTestSturct() *testStruct {
    if instance == nil {
        num++
        time.Sleep(500 * time.Millisecond)
        instance = &testStruct{num: num}
    }
    return instance
}

func createAndCheckSingleton() {
    ins := NewTestSturct()
    fmt.Printf("ins 的地址为 %p, ins 的 num 属性值期望为 1,实际为 %d\n", ins, ins.num)
}

func main() {
    go createAndCheckSingleton()
    go createAndCheckSingleton()
    time.Sleep(1 * time.Second)
}

上面代码看似没问题,但是我们执行一下,看输出结果:

ins 的地址为 0xc000184000, ins 的 num 属性值期望为 1,实际为 2
ins 的地址为 0xc000094000, ins 的 num 属性值期望为 1,实际为 2

我们并不能得到我们期望的输出,这是因为 NewTestSturct 函数比较耗时,在 testStruct 还没完全初始化时别的 go 程再次调用了这个函数。

注意:上面两个 time.Sleep,第一个 time.Sleep 是为了模拟耗时长的临界资源操作,第二个 time.Sleep 是为了防止主 go 程过早退出。

加锁的单例模式

sync.Mutex
package main

import (
    "fmt"
    "sync"
    "time"
)

type testStruct struct {
    num int
}

var mu sync.Mutex
var instance *testStruct
var num int = 0

func NewTestSturct() *testStruct {
    mu.Lock()
    defer mu.Unlock()
    if instance == nil {
        num++
        time.Sleep(500 * time.Millisecond)
        instance = &testStruct{num: num}
    }
    return instance
}

func createAndCheckSingleton() {
    ins := NewTestSturct()
    fmt.Printf("ins 的地址为 %p, ins 的 num 属性值期望为 1,实际为 %d\n", ins, ins.num)
}

func main() {
    go createAndCheckSingleton()
    go createAndCheckSingleton()
    time.Sleep(1 * time.Second)
}

输出结果:

ins 的地址为 0xc000180000, ins 的 num 属性值期望为 1,实际为 1
ins 的地址为 0xc000180000, ins 的 num 属性值期望为 1,实际为 1

通过加锁的方式,确保了 NewTestSturct 不会在多个 go 程中并发运行,这次我们得到了我们想要的结果。

sync.Once 更优雅的方式实现单例模式

sync.Once
package main

import (
    "fmt"
    "sync"
    "time"
)

type testStruct struct {
    num int
}

var once sync.Once
var instance *testStruct
var num int = 0

func NewTestSturct() *testStruct {
    once.Do(func() {
        num++
        time.Sleep(500 * time.Millisecond)
        instance = &testStruct{num: num}
    })
    return instance
}

func createAndCheckSingleton() {
    ins := NewTestSturct()
    fmt.Printf("ins 的地址为 %p, ins 的 num 属性值期望为 1,实际为 %d\n", ins, ins.num)
}

func main() {
    go createAndCheckSingleton()
    go createAndCheckSingleton()
    time.Sleep(1 * time.Second)
}

sync.Once.Do 的参数是一个函数,对于同一个 sync.Once 的变量 once 无论调用多少次 once.Do 只有第一次调用是执行了其参数传递进来的函数的。

输出结果:

ins 的地址为 0xc000100000, ins 的 num 属性值期望为 1,实际为 1
ins 的地址为 0xc000100000, ins 的 num 属性值期望为 1,实际为 1

更优雅的方式防止主 go 程提前退出

在上面的例子中我使用了 time.Sleep 来防止主 go 程在其他 go 程还没运行完的情况下提前结束,其实这是一种非常非常笨拙的方式,我们没法准确的把握需要 sleep 的时间,强烈建议大家不要在正式代码中这么使用。

接下面我们介绍一个优雅的方法 sync.WaitGroup,我们直接看示例代码:

package main

import (
    "fmt"
    "sync"
    "time"
)

type testStruct struct {
    num int
}

func testWaitGroup(wg *sync.WaitGroup) {
    defer wg.Done()
    time.Sleep(1 * time.Second)
    fmt.Println("子 go 程已经执行完毕,我的父 go 程可以退出了")
}

func main() {
    var wg sync.WaitGroup = sync.WaitGroup{}
    wg.Add(1)
    go testWaitGroup(&wg)
    wg.Wait()
}

输出结果:

 go 程已经执行完毕,我的父 go 程可以退出了

看见没,这里我们就没用 sleep,而且父 go 程也能及时的知道子 go 程执行完成了。但是在使用 sync.WaitGroup 时有两点需要特别注意

1、wg.Add(1) 这个语句如果放在子 go 程函数开始的位置可以吗?我们来直接看例子:

package main

import (
    "fmt"
    "sync"
    "time"
)

type testStruct struct {
    num int
}

func testWaitGroup(wg *sync.WaitGroup) {
    wg.Add(1)
    defer wg.Done()
    time.Sleep(1 * time.Second)
    fmt.Println("子 go 程已经执行完毕,我的父 go 程可以退出了")
}

func main() {
    var wg sync.WaitGroup = sync.WaitGroup{}
    go testWaitGroup(&wg)
    wg.Wait()
}
wg.Add(1)

2、函数 testWaitGroup 的参数能不使用指针吗?还是直接看例子:

package main

import (
    "fmt"
    "sync"
    "time"
)

type testStruct struct {
    num int
}

func testWaitGroup(wg sync.WaitGroup) {
    defer wg.Done()
    time.Sleep(1 * time.Second)
    fmt.Println("子 go 程已经执行完毕,我的父 go 程可以退出了")
}

func main() {
    var wg sync.WaitGroup = sync.WaitGroup{}
    wg.Add(1)
    go testWaitGroup(wg)
    wg.Wait()
}

输出结果:

子 go 程已经执行完毕,我的父 go 程可以退出了
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc000016180?)
        /usr/local/go/src/runtime/sema.go:56 +0x25
sync.(*WaitGroup).Wait(0x0?)
        /usr/local/go/src/sync/waitgroup.go:136 +0x52
main.main()
        /root/yjfwk/testgo/main.go:23 +0x7d
exit status 2

可以看到在子 go 程执行结束后,程序抛出了 deadlock 的异常。这是为什么呢?其实答案就隐藏在我之前的文章中:golang 函数参数传递–指针,引用和值(二)。

因为 sync.WaitGroup 是一个结构体,结构体是一个值类型的变量,在函数参数传递中值类型的变量会发送 copy,也就是说不使用指针子 go 程和父 go 程的的 wg 已经不是同一个了,在子 go 程中执行 wg.Done() 并不会影响到其父 go 程,因此子 go 程退出后父 go 程还继续尝试等待但是系统系统检测到没有其他 go 程了等下去是无意义的,所以就抛出了异常。

sync.Cond–golang 指挥家

sync.Condsync.Condsync.CondWaitSignalBroadcastWaitSignalSignalBroadcastBroadcast
package main

import (
    "fmt"
    "sync"
    "time"
)

type testStruct struct {
    num int
}

func testWaitGroup(wg *sync.WaitGroup, conductor *sync.Cond) {
    defer wg.Done()
    conductor.L.Lock()
    defer conductor.L.Unlock()
    fmt.Printf("%s 等待信号\n", time.Now().Format("2006-01-02 15:04:05"))
    conductor.Wait()
    fmt.Printf("%s 收到信号\n", time.Now().Format("2006-01-02 15:04:05"))
}

func main() {
    var wg sync.WaitGroup
    conductor := sync.NewCond(&sync.Mutex{})
    wg.Add(3)
    go testWaitGroup(&wg, conductor)
    go testWaitGroup(&wg, conductor)
    go testWaitGroup(&wg, conductor)
    time.Sleep(5 * time.Second)
    conductor.Broadcast()
    wg.Wait()
}

输出结果:

2022-09-02 11:52:26 等待信号
2022-09-02 11:52:26 等待信号
2022-09-02 11:52:26 等待信号
2022-09-02 11:52:31 收到信号
2022-09-02 11:52:31 收到信号
2022-09-02 11:52:31 收到信号

读者可以自己吧 conductor.Broadcast() 换成 conductor.Signal() 试一试。

sync.WaitGroupsync.Cond
sync
sync.RWMutexsync.Mutexsync.Mapmapsync.Mapsync.Mapsync.Pool