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:为什么我们会在添加之前解锁?
答案 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很容易解决。使用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。它是如何工作的,为什么它有用?
RWMutex是Mutex的扩展,并添加了两种方法:RLock和RUnlock。关于RWMutex:
,有几点值得注意RLock是共享读锁。当用它进行锁定时,其他线程*也可以使用RLock自行锁定。这意味着多个线程*可以同时读取。它是半独家的。
如果锁定互斥锁,则会阻止对Lock的呼叫**。如果一个或多个读者持有锁定,则无法写入。
考虑它的一个好方法是RWMutex是带有读者计数器的Mutex。 RLock递增计数器,而RUnlock递减计数器。只要该计数器是>,对Lock的调用就会阻止。 0
您可能会想:如果我的应用程序读得很重,那是否意味着作者可以无限期地被阻止?不。RWMutex还有一个有用的属性:
将其视为杂货店登记册上方的灯光,表示收银员是否开放。排队的人会留在那里,他们会得到帮助,但新人不能排队。一旦最后剩下的客户得到帮助,收银员就会休息,并且该登记册要么一直关闭,直到他们回来或者他们被另一个收银员替换。
让我们使用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
}
通过上面的代码,我将逻辑分为getCounter
和initCounter
函数:
上面的代码与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。但是,当你使用线程时,你不必像使用常规例程一样保守。
**
当Lock
,RLock
,频道或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