用喜欢和舒服的方式在Golang中使用锁、使用channel自定义锁

众所周知,我们能使用Golang轻松编写并发程序。Golang利用goroutine,让我们编写并发程序变得容易。并发程序中重要的问题之一就是如何正确的处理“竞争资源”或“共享资源”。Golang为我们提供了锁的机制。这篇文章,就简单介绍Golang中锁的使用方法。并且进行错误的使用方法和正确的使用方法的代码示例对比。文章的所以代码示例在:https://github.com/pathbox/learning-go/tree/master/src/lock

原文链接

我们看第一个栗子:

package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    Value int
}

var wg sync.WaitGroup
var mutex sync.Mutex // 声明了一个全局锁
func main() {

    wg.Add(1000)
    counter := &Counter{Value: 0}

    for i := 0; i < 1000; i++ {
        go Count(counter, mutex)
    }
    wg.Wait()
    fmt.Println("Count Value: ", counter.Value)
}

func Count(counter *Counter, mutex sync.Mutex) {
    mutex.Lock()
    defer mutex.Unlock()
    counter.Value++
    wg.Done()
}

/*
输出结果:
Count Value:  982
*/

这里声明了一个全局锁 sync.Mutex,然后将这个全局锁以参数的方式代入到方法中,这样并没有真正起到加锁的作用。

正确的方式是:

package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    Value int
}

var wg sync.WaitGroup
var mutex sync.Mutex // 声明了一个全局锁
func main() {

    wg.Add(1000)
    counter := &Counter{Value: 0}

    for i := 0; i < 1000; i++ {
        go Count(counter)
    }
    wg.Wait()
    fmt.Println("Count Value: ", counter.Value)
}

func Count(counter *Counter) {
    mutex.Lock()
    defer mutex.Unlock()
    counter.Value++
    wg.Done()
}

/*
输出结果:
Count Value:  1000
*/

声明了一个全局锁后,其作用范围是全局。直接使用,而不是将其作为参数传递到方法中。

下一个栗子

package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    Value int
}

var wg sync.WaitGroup

func main() {
    var mutex sync.Mutex // 声明了一个非全局锁
    wg.Add(1000)
    counter := &Counter{Value: 0}

    for i := 0; i < 1000; i++ {
        go Count(counter, mutex)
    }
    wg.Wait()
    fmt.Println("Count Value: ", counter.Value)
}

func Count(counter *Counter, mutex sync.Mutex) {
    mutex.Lock()
    defer mutex.Unlock()
    counter.Value++
    wg.Done()
}

/*
输出结果:
Count Value:  954
*/

上面栗子中,声明的不是全局锁。然后将这个锁作为参数传入到Count()方法中,这样并没有真正起到加锁的作用。

正确的方式:

package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    Value int
}

var wg sync.WaitGroup

func main() {
    mutex := &sync.Mutex{} // 定义了一个锁 mutex,赋值给mutex
    wg.Add(1000)
    counter := &Counter{Value: 0}

    for i := 0; i < 1000; i++ {
        go Count(counter, mutex)
    }
    wg.Wait()
    fmt.Println("Count Value: ", counter.Value)
}

func Count(counter *Counter, mutex *sync.Mutex) {
    mutex.Lock()
    defer mutex.Unlock()
    counter.Value++
    wg.Done()
}

/*
输出结果:
Count Value:  1000
*/

这次通过 mutex := &sync.Mutex{},定义了mutex,然后作为参数传递到方法中,正确实现了加锁功能。

简单的说,在全局声明全局锁,之后这个全局锁就能在代码中的作用域范围内都能使用了。但是,也许你需要的不是全局锁。这和锁的粒度有关。 所以,你可以声明一个锁,在其作用域范围内使用,并且这个作用域范围是有并发执行的,别将锁当成参数传递。如果,需要将锁当成参数传递,那么你传的不是一个锁的声明,而是这个锁的指针。

下面,我们讨论一种更好的使用方式。通过阅读过很多”牛人“写的Go的程序或源码库,在锁的使用中。常常将锁放入对应的 struct 中定义,我觉得这是一种不错的方法。

package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    Value int
    sync.Mutex
}

var wg sync.WaitGroup

func main() {

    wg.Add(1000)
    counter := &Counter{Value: 0}

    for i := 0; i < 1000; i++ {
        go Count(counter)
    }
    wg.Wait()
    fmt.Println("Count Value: ", counter.Value)
}

func Count(counter *Counter) {
    counter.Lock()
    defer counter.Unlock()
    counter.Value++
    wg.Done()
}

/*
输出结果:
Count Value:  1000
*/

这样,我们声明的不是全局锁,并且这个需要加锁的竞争资源也正是 struct Counter 本身的Value属性,反映了这个锁的粒度。我觉得这是一种很舒服的使用方式(暂不知道这种方式会带来什么负面影响,如果有踩过坑的朋友,欢迎聊一聊这个坑),当然,如果你需要全局锁,那么请定义全局锁。

还可以有更多的使用方式:

// 1.
type Counter struct {
   Value int
   Mutex sync.Mutex
}

counter := &Counter{Value: 0}
counter.Mutex.Lock()
defer counter.Mutex.Unlock()

//2.
type Counter struct {
   Value int
   Mutex *sync.Mutex
}

counter := &Counter{Value: 0, Mutex: &sync.Mutex{}}
counter.Mutex.Lock()
defer counter.Mutex.Unlock()

Choose the way you like~

接下来,我们自己尝试创建一个互斥锁。

简单的说,简单的互斥锁锁的原理是:一个线程(进程)拿到了这个互斥锁,在这个时刻,只有这个线程(进程)能够进行互斥锁锁的范围中的"共享资源"的操作,主要是写操作。我们这里不讨论读锁的实现。锁的种类很多,有不同的实现场景和功能。这里我们讨论的是最简单的互斥锁。

我们能够利用Golang 的channel所具有特性,创建一个简单的互斥锁。

/locker/locker.go

package locker

// Mutext struct
type Mutex struct {
    lock chan struct{}
}

// 创建一个互斥锁
func NewMutex() *Mutex {
    return &Mutex{lock: make(chan struct{}, 1)}
}

// 锁操作
func (m *Mutex) Lock() {
    m.lock <- struct{}{}
}

// 解锁操作
func (m *Mutex) Unlock() {
    <-m.lock
}

main.go

package main

import (
    "./locker"
    "fmt"
    "time"
)

type record struct {
    lock          *locker.Mutex
    lock_count    int
    no_lock_count int
}

func newRecord() *record {
    return &record{
        lock:          locker.NewMutex(),
        lock_count:    0,
        no_lock_count: 0,
    }
}

func main() {
    r := newRecord()

    for i := 0; i < 1000; i++ {
        go CountWithoutLock(r)
        go CountWithLock(r)
    }
    time.Sleep(2 * time.Second)
    fmt.Println("Record no_lock_count: ", r.no_lock_count)
    fmt.Println("Record lock_count: ", r.lock_count)
}

func CountWithLock(r *record) {
    r.lock.Lock()
    defer r.lock.Unlock()
    r.lock_count++
}

func CountWithoutLock(r *record) {
    r.no_lock_count++
}

/* 输出结果
Record no_lock_count:  995
Record lock_count:  1000
*/

locker 就是通过使用channel的读操作和写操作会互相阻塞等待的这个同步性质。 可以简单的理解为,channel中传递的就是互斥锁。一个线程(进程)申请了一个互斥锁(struct{}{}),将这个互斥锁存放在channel中, 其他线程(进程)就没法申请互斥锁放入channel,而处于阻塞状态,等待channel恢复空闲空间。该线程(进程)进行操作”共享资源“,然后释放这个互斥锁(从channel中取走),channel这时候恢复了空闲的空间,其他线程(进程) 就能申请互斥锁并且放入channel。这样,在某一时刻,只会有一个线程(进程)拥有互斥锁,在操作"共享资源"。

4 个评论

将锁放入对应的 struct 中定义,前提是必须使用struct的引用
可否举一个struct中定义锁 加锁不成功的代码例子
不正确使用肯定会不成功的,如每一次都new一个新的对象
每个 main goroutine 都会有一个对象,许多子 goroutine 操作会并发操作这个对象,这时候,在对象 struct 中定义一个锁,使用时成功的。并不会 在子goroutine 中再new一个struct, 这样在子goroutine new 的struct 本身是独立的,毫不相干的。对这个struct操作,也不会有竞争情况。不知道 你指的是这个情况吗

要回复文章请先登录注册