如何在Golang中使用RWMutex?

时间:2013-10-03 00:10:35

标签: go

type Stat struct {
    counters     map[string]*int64
    countersLock sync.RWMutex
    averages     map[string]*int64
    averagesLock sync.RWMutex
}

以下称为

func (s *Stat) Count(name string) {
    s.countersLock.RLock()
    counter := s.counters[name]
    s.countersLock.RUnlock()
    if counter != nil {
        atomic.AddInt64(counter, int64(1))
        return
    }
}

我的理解是我们首先锁定接收器s(这是一种类型Stat)然后如果计数器存在则添加它。

问题:

Q1:为什么我们需要锁定它? RWMutex甚至意味着什么?

Q2:s.countersLock.RLock() - 这会锁定整个接收器还是只锁定Stat类型中的计数器字段?

问题3:s.countersLock.RLock() - 这会锁定平均值字段吗?

问题4:我们为什么要使用RWMutex?我认为channel是在Golang中处理并发的首选方法吗?

问题5:这是atomic.AddInt64。在这种情况下,为什么我们需要原子?

问题6:为什么我们会在添加之前解锁?

2 个答案:

答案 0 :(得分:62)

当多个线程*需要改变相同的值时,需要一个锁定机制来同步访问。没有它,两个或多个线程*可能同时写入相同的值,导致内存损坏,通常会导致崩溃。

atomic包提供了一种快速简便的方法来同步对原始值的访问。对于计数器,它是最快的同步方法。它具有定义明确的用例的方法,例如递增,递减,交换等。

sync包提供了一种同步访问更复杂值的方法,例如地图,切片,数组或值组。您可以将此用于atomic中未定义的用例。

在任何一种情况下,只有在写作时才需要锁定。多个线程*可以在没有锁定机制的情况下安全地读取相同的值。

让我们来看看你提供的代码。

type Stat struct {
    counters     map[string]*int64
    countersLock sync.RWMutex
    averages     map[string]*int64
    averagesLock sync.RWMutex
}

func (s *Stat) Count(name string) {
    s.countersLock.RLock()
    counter := s.counters[name]
    s.countersLock.RUnlock()
    if counter != nil {
        atomic.AddInt64(counter, int64(1))
        return
    }
}

这里缺少的是地图本身的初始化方式。到目前为止,这些地图并未发生变异。如果计数器名称是预先确定的,并且以后无法添加,则您不需要RWMutex。该代码可能如下所示:

type Stat struct {
    counters map[string]*int64
}

func InitStat(names... string) Stat {
    counters := make(map[string]*int64)
    for _, name := range names {
        counter := int64(0)
        counters[name] = &counter
    }
    return Stat{counters}
}

func (s *Stat) Count(name string) int64 {
    counter := s.counters[name]
    if counter == nil {
        return -1 // (int64, error) instead?
    }
    return atomic.AddInt64(counter, 1)
}

(注意:我删除了平均值,因为它没有在原始示例中使用。)

现在,让我们说你不希望你的计数器被预先确定。在这种情况下,您需要一个互斥锁来同步访问。

让我们只用Mutex来试试吧。这很简单,因为一次只有一个线程*可以容纳Lock。如果第二个线程*在第一个线程*尝试Lock之前尝试Unlock,那么它会等待(或阻止)**直到那时。

type Stat struct {
    counters map[string]*int64
    mutex    sync.Mutex
}

func InitStat() Stat {
    return Stat{counters: make(map[string]*int64)}
}

func (s *Stat) Count(name string) int64 {
    s.mutex.Lock()
    counter := s.counters[name]
    if counter == nil {
        value := int64(0)
        counter = &value
        s.counters[name] = counter
    }
    s.mutex.Unlock()
    return atomic.AddInt64(counter, 1)
}

上面的代码可以正常工作。但是有两个问题。

  1. 如果Lock()和Unlock()之间出现混乱,即使您要从恐慌中恢复,互斥锁也会永久锁定。这段代码可能不会引起恐慌,但总的来说,假设它可能是更好的做法。
  2. 取出计数器时会进行独占锁定。一次只能从计数器读取一个线程*。
  3. 问题#1很容易解决。使用defer

    func (s *Stat) Count(name string) int64 {
        s.mutex.Lock()
        defer s.mutex.Unlock()
        counter := s.counters[name]
        if counter == nil {
            value := int64(0)
            counter = &value
            s.counters[name] = counter
        }
        return atomic.AddInt64(counter, 1)
    }
    

    这确保始终调用Unlock()。如果由于某种原因你有多个返回,你只需要在函数的头部指定一次Unlock()。

    可以使用RWMutex解决问题#2。它是如何工作的,为什么它有用?

    RWMutexMutex的扩展,并添加了两种方法:RLockRUnlock。关于RWMutex

    ,有几点值得注意
    • RLock是共享读锁。当用它进行锁定时,其他线程*也可以使用RLock自行锁定。这意味着多个线程*可以同时读取。它是半独家的。

    • 如果锁定互斥锁,则会阻止对Lock的呼叫**。如果一个或多个读者持有锁定,则无法写入。

    • 如果互斥锁被写入锁定(使用Lock),则RLock将阻止**。

    考虑它的一个好方法是RWMutex是带有读者计数器的MutexRLock递增计数器,而RUnlock递减计数器。只要该计数器是>,对Lock的调用就会阻止。 0

    您可能会想:如果我的应用程序读得很重,那是否意味着作者可以无限期地被阻止?不。RWMutex还有一个有用的属性:

    • 如果读者计数器是> 0和Lock被调用,未来对RLock的调用也会阻塞,直到现有读者释放了他们的锁,作者已获得锁并稍后释放它。

    将其视为杂货店登记册上方的灯光,表示收银员是否开放。排队的人会留在那里,他们会得到帮助,但新人不能排队。一旦最后剩下的客户得到帮助,收银员就会休息,并且该登记册要么一直关闭,直到他们回来或者他们被另一个收银员替换。

    让我们使用RWMutex修改前面的示例:

    type Stat struct {
        counters map[string]*int64
        mutex    sync.RWMutex
    }
    
    func InitStat() Stat {
        return Stat{counters: make(map[string]*int64)}
    }
    
    func (s *Stat) Count(name string) int64 {
        var counter *int64
        if counter = getCounter(name); counter == nil {
            counter = initCounter(name);
        }
        return atomic.AddInt64(counter, 1)
    }
    
    func (s *Stat) getCounter(name string) *int64 {
        s.mutex.RLock()
        defer s.mutex.RUnlock()
        return s.counters[name]
    }
    
    func (s *Stat) initCounter(name string) *int64 {
        s.mutex.Lock()
        defer s.mutex.Unlock()
        counter := s.counters[name]
        if counter == nil {
            value := int64(0)
            counter = &value
            s.counters[name] = counter    
        }
        return counter
    }
    

    通过上面的代码,我将逻辑分为getCounterinitCounter函数:

    • 保持代码简单易懂。 RLock()和Lock()在同一个函数中很难。
    • 使用延迟时尽快释放锁。

    上面的代码与Mutex示例不同,允许您同时增加不同的计数器。

    我想指出的另一件事是上面的所有例子,地图map[string]*int64包含指向计数器的指针,而不是计数器本身。如果您要将计数器存储在地图map[string]int64中,则需要使用Mutex而不使用atomic。该代码看起来像这样:

    type Stat struct {
        counters map[string]int64
        mutex    sync.Mutex
    }
    
    func InitStat() Stat {
        return Stat{counters: make(map[string]int64)}
    }
    
    func (s *Stat) Count(name string) int64 {
        s.mutex.Lock()
        defer s.mutex.Unlock()
        s.counters[name]++
        return s.counters[name]
    }
    

    您可能希望这样做以减少垃圾收集 - 但这只会在您拥有数千个计数器时才会起作用 - 即使这样,计数器本身也不会占用大量空间(相比之下字节缓冲区)。

    *当我说线程时我的意思是常规。其他语言的线程是一种同时运行一组或多组代码的机制。创建和拆除线程的成本很高。 go-routine建立在线程之上,但重用它们。当一个go例程休眠时,底层线程可以被另一个例程使用。当一个go-routine唤醒时,它可能在另一个线程上。 Go在幕后处理所有这些。 - 但是对于所有意图和目的,当涉及到内存访问时,你会像线程一样处理一个go-routine。但是,当你使用线程时,你不必像使用常规例程一样保守。

    **LockRLock,频道或Sleep阻止了常规例程时,可能会重复使用基础线程。该例程没有使用cpu - 将其视为排队等候。像其他语言一样,像for {}这样的无限循环会阻塞,同时保持cpu和常规忙碌 - 想到这就像在一个圆圈中跑来跑去 - 你会晕眩,呕吐,和周围的人一起不高兴。

答案 1 :(得分:30)

  

问题:

     

Q1:为什么我们需要锁定它? RWMutex甚至意味着什么?

RW代表读/写。 CF doc:http://golang.org/pkg/sync/#RWMutex

我们需要锁定它以防止其他例程/线程在我们处理它时更改值。

  

Q2:s.countersLock.RLock() - 这会锁定整个接收器   或只有Stat?

类型的计数器字段

作为互斥锁,只有在调用RLock()函数时才会发生锁定。如果任何其他goroutine已经调用了WLock(),那么它就会阻塞。你可以在同一个goroutine中调用任意数量的RLock(),它不会锁定。

因此它不会锁定任何其他字段,甚至不会s.counters。在您的示例中,您锁定地图查找以找到正确的计数器。

  

问题3:s.countersLock.RLock() - 这会锁定平均值字段吗?

不,如第二季度所述,RLock仅锁定自己。

  

问题4:我们为什么要使用RWMutex?我认为频道是首选方式   在Golang中处理并发性?

频道非常有用,但有时它还不够,有时它没有意义。

在这里,当您锁定地图访问时,互斥锁是有意义的。使用chan,你必须有1缓冲的chan,之前发送和之后接收。不太直观。

  

问题5:这是atomic.AddInt64。在这种情况下,为什么我们需要原子?

此函数将以原子方式递增给定变量。在您的情况下,您有一个竞争条件:counter是一个指针,实际变量可以在锁定释放之后和调用atomic.AddInt64之前销毁。 如果你不熟悉这类东西,我建议你坚持使用互斥锁,并在锁定/解锁之间进行所需的所有处理。

  

问题6:为什么我们会在添加之前解锁?

你不应该。

我不知道你要做什么,但这是一个(简单)例子:https://play.golang.org/p/cVFPB-05dw