如何使用 RWMutex?

新手上路,请多包涵
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() 这会锁定整个 receiver s 还是只锁定 Stat 类型的 counters 字段?

Q3: s.countersLock.RLock() 这会锁定平均值字段吗?

Q4:为什么要使用 RWMutex ?我认为通道是 Golang 中处理并发的首选方式?

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

Q6:为什么我们要在添加之前解锁?

原文由 samol 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 577
2 个回答

问题:

Q1:为什么需要加锁? RWMutex 是什么意思?

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

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

Q2: s.countersLock.RLock() 这会锁定整个 receiver 还是仅锁定 Stat 类型的计数器字段?

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

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

Q3: s.countersLock.RLock() 这会锁定平均值字段吗?

不,正如 Q2 中所说,a RLock 只锁定了他自己。

Q4:为什么要用 RWMutex ?我认为通道是 Golang 中处理并发的首选方式?

Channel 非常有用,但有时不够用,有时又没有意义。

在这里,当您锁定地图访问权限时,互斥量很有意义。对于 chan,您必须有一个缓冲的 1 通道,先发送后接收。不是很直观。

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

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

Q6:为什么我们要在添加之前解锁?

你不应该。

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

原文由 creack 发布,翻译遵循 CC BY-SA 4.0 许可协议

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

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

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 。如果第二个线程\* 在第一个线程使用 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. 获取计数器时会使用独占锁。一次只有一个线程*可以从计数器读取。

问题 #1 很容易解决。使用 延迟

 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() 。

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

RWMutexMutex 的扩展,增加了两个方法: RLockRUnlock 。关于 RWMutex 有几点需要注意:

  • RLock 是共享 _读锁_。当锁定时,其他线程*也可以使用 RLock 锁定自己的锁。这意味着多个线程*可以同时读取。它是半排他性的。

  • 如果互斥量被读取锁定,则对 Lock 的调用将被阻止**。如果一个或多个读者持有锁,则无法写入。

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

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

您可能会想:如果我的应用程序读取量很大,是否意味着作者可能会被无限期地阻止?不。 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()。
  • 使用 defer 时尽早释放锁。

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

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

 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-routine 建立在线程之上,但重复使用它们。当一个 go-routine 休眠时,底层线程可以被另一个 go-routine 使用。当 go-routine 唤醒时,它可能在不同的线程上。 Go 在幕后处理这一切。 – 但就所有意图和目的而言,当涉及到内存访问时,您会将 go-routine 视为线程。但是,在使用 go-routines 时不必像使用线程那样保守。

** 当 go-routine 被 LockRLock 、通道或 Sleep 阻塞时,底层线程可能会被重新使用。该 go-routine 没有使用 cpu - 将其视为排队等候。像其他语言一样,像 for {} 这样的无限循环会阻塞,同时保持 cpu 和 go-routine 忙碌 - 把它想象成一个圆圈 - 你会头晕,呕吐,你周围的人不会很开心。

原文由 Luke 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题