|
type Stat struct counters map[string]*int64 countersLock sync.RWMutex averages map[string]*int64 averagesLock sync.RWMutex}
6 R* M8 E3 T6 a7 a) Q2 o& ^ 下面叫它
) Z1 L5 j( H& A1 Y1 p. K3 W! F4 h# f8 D
- 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 code]我的理解是,我们首先锁定接收者 s(这是 Stat 类型),如果计数器确实存在,我们会添加它。5 B }/ I, X& F. L9 |' [
- 问题:
' j8 t4 b- U8 O* H0 Y" a+ P) W - Q为什么要加锁?RWMutex甚至是什么意思?, v+ J$ b3 @' w* p3 ^9 a# I# [, ]0 ?
- Q2:s.countersLock.RLock()- 会锁定整个接收者还是只锁定 Stat 类型的 counters 字段?# G4 m+ q/ Z" b6 b4 G4 V
- Q3:s.countersLock.RLock()- 这会锁定平均值字段吗?
( z6 j y# K6 j - Q为什么要用?RWMutex?我认为通道在 Golang 中处理并发的首选方式?
- m; [+ k* c8 d/ [ - Q5:这是什么atomic.AddInt64.为什么我们在这种情况下需要原子?
3 J' i- P! Y" ^ - Q6:为什么要在添加之前立即解锁?0 A z% Y9 P4 a8 S
- 8 M+ J$ q$ V G: Y) o- l1 S
- 解决方案: & r& U! d2 X$ q2 Q
- 当多个线程当需要改变相同的值时,需要同步访问的锁定机制。如果没有两个或两个以上的线程它可能同时写入相同的值,导致内存损坏,通常导致崩溃。; `9 v) C0 r( X6 z4 N8 T
- 原子包为同步访问提供了一种快速简单的方法。它是计数器最快的同步方法。它有一种定义明确用例的方法,如增加、减少、交换等。
3 i8 p: c* c/ f. S# m0 N5 Q* q- ~ - 同步包提供了一个更复杂的同步访问值,如地图、切片、阵列或组。您可以将其用于未存在atomic 中定义的用例。
" d) l3 V/ p$ J) h4 ?: Z. J - 在任何情况下,只有在写入时才需要锁定。*在没有锁定机制的情况下,可以安全地读取相同的值。; e/ ^+ @. v, J1 u5 C
- 让我们看看您提供的代码。& S! u0 q3 i! \* o# r2 u( b3 x5 n
- [code]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 code]缺少的是地图本身是如何初始化的。到目前为止,这些地图还没有发生变化。如果计数器的名称是提前确定的,以后不能添加,则无需RWMutex。该代码可能如下:[code]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)}1 B) Q- R; A! a5 B2 m5 m) R, F+ L4 i
(注:我删除了平均值,因为它没有在原始示例中使用。, Y. g6 P2 z/ U. O9 D" U: y. H
现在,假设你不希望你的计数器提前确定。在这种情况下,您需要斥锁来同步访问。
3 `& |- v3 W: F( b* k8 k; T让我们只用一个Mutex试试吧。这很简单,因为一次只有一个线程可以持有Lock。假如第二线程在试图锁定第一个版本之前,他们等待(或块)**那时。
$ {9 M7 W" w! ntype 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)}
3 D+ z T8 e9 _ ~! q" Y, j 上述代码可以正常工作。但是有两个问题。- j8 U6 r/ J( u7 z6 _! Q' C! C& O
[ol]如果 Lock() 和 Unlock() 如果有恐慌,即使你想从恐慌中恢复,互斥也会永远锁定。这个代码可能不会恐慌,但一般来说,假设可能更好。, b* X: L0 V, i. G' m
获取计数器时获得排他锁。一次只有一个线程*它可以从计数器中读取。[/ol]问题#1 易于解决。延迟使用:
7 _% s1 t8 }7 D9 f, hfunc (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)}
* B- r2 v2 {/ w6 C 这样可以保证始终调用 Unlock()。如果由于某种原因你有不止一个返回,你只需要在函数的开头指定 Unlock() 一次。
' p$ ?* c: G/ e) f问题#2 可以用RWMutex解决。它到底是怎么工作的,为什么有用?
' F, R( m% ^+ N9 ~RWMutex是Mutex扩展增加了两种方法:RLock和RUnlock。关于RWMutex需要注意的几点:
9 T+ z$ }1 }) K4 y& x) a9 u l: YRLock是共享读锁。当锁被拿走时,其他线程也可以用RLock拿走自己的锁。这意味着多个线程可同时读取。它是半排他性的。
3 x, L7 C+ C0 w1 j6 K. j' {互斥锁被读取锁定的,对Lock停止调用**。如果一个或多个读者持有一个锁,你就不能写了。- A$ v- l4 W( _: ]* P# ]6 _. I
如果互斥锁被写锁(使用)Lock),RLock将阻塞**。一个好的思维方式是RWMutex带读取器计数器的互斥锁。RLock和RUnlock减少它。只要计数器 > 0,对Lock调用会堵塞。
9 T# l; n" S6 p+ }+ ?1 ]你可能会想:如果我的应用程序被大量读取,这是否意味着写入器可能被无限期阻塞?RWMutex还有一个有用的属性:
+ m" A/ y: e$ V( ?: C- [3 A% N' `如果读者计数器 > 0 并且Lock调用,以后对RLock 的调用也会被阻止,直到现有读者释放他们的锁,作者得到他的锁并稍后释放它。把它想象成杂货店收银台上方的灯,上面显示收银员是否营业。排队的人可以留在那里,他们会得到帮助,但新人不能排队。一旦最后剩下的顾客得到帮助,收银员就会休息,并且该收银机要么保持关闭,直到他们回来,要么被另一个收银员取代。
O, u# x, d0 H% n! J; N( l P6 w1 g+ Z让我们用RWMutex修改前面的例子:
* Y+ z7 {6 s$ C& j- etype 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}3 L% l- G2 n* M8 x% M+ `3 n
使用上述代码,我将逻辑分离为getCounter和initCounter函数:
1 s% c! k7 h' Q4 x5 @, |保持代码简单易懂。在同一函数中使用 RLock() 和 Lock() 会很难。1 [, i; z: j1 C* f7 n
使用 defer 尽早释放锁。与Mutex不同的示例允许您同时添加不同的计数器。
( f! E. W/ D$ g) ]5 ?我想指出的另一件事是映射map[string]*int64包含指向计数器的指针,而不是计数器本身。如果要计数器存储在地图中map[string]int64不需要使用atomic 的Mutex。代码如下:9 d0 e R6 K1 i# J- C$ v
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]}
% W; V% p1 I% w0 V& D 你可能想这样做来减少收集 - 但这只在你有数千个计数器的时候才重要- 即便如此,计数器本身也不会占用很多空间(与字节缓冲区等东西相比)。3 S" L# t% `9 F
*当我说线程时,我的意思是 go-routine。其他语言中的线程是一种同时运行一组或多组代码的机制。创建和拆除线程的成本很高。go-routine 是基于线程的,但会重用。go-routine 休眠时,底线可以是另一个 go-routine 使用。当一个 go-routine 醒来时,它可能在不同的线程上。Go 在幕后处理一切。– 但是,当涉及到内存访问时,出于所有的意图和目的,你会 go-routine 视为线程。然而, 正在使用go-routines 不必像使用线程那样保守。! |7 B# P0 y& a8 s9 h" e
**当 go-routine 被Lock、RLock、通道或Sleep当堵塞时,底层线程可能会被重用。go-routine 不使用 CPU - 将其视为排队等候。像其他语言一样,无限循环for {}同时保持 cpu 和 go-routine 忙碌 |
|