回答

收藏

如何使用 RWMutex?

技术问答 技术问答 354 人阅读 | 0 人回复 | 2023-09-12

    type Stat struct    counters     map[string]*int64    countersLock sync.RWMutex    averages     map[string]*int64    averagesLock sync.RWMutex}- ]- |2 i, L* v4 C, p; t; p
下面叫它. [% J; o6 A7 m: b% r# I4 S
    / {3 l' q7 }2 k- U( n7 w
  • 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 类型),如果计数器确实存在,我们会添加它。
    6 D4 u- B6 G/ n, B' Q# U3 Q1 l
  • 问题:
    0 O0 Z  Z3 G9 e* v: k
  • Q为什么要加锁?RWMutex甚至是什么意思?
    6 X' U# f5 l9 h0 i
  • Q2:s.countersLock.RLock()- 会锁定整个接收者还是只锁定 Stat 类型的 counters 字段?
    9 o, c1 u! ^- [, C$ f  g
  • Q3:s.countersLock.RLock()- 这会锁定平均值字段吗?
    6 e- I* O& [1 r0 }! d; Q! h3 V
  • Q为什么要用?RWMutex?我认为通道在 Golang 中处理并发的首选方式?
    : ^$ Z  |+ X; F
  • Q5:这是什么atomic.AddInt64.为什么我们在这种情况下需要原子?2 y6 c7 j) z, o1 K4 P6 v, Z- g
  • Q6:为什么要在添加之前立即解锁?
    ; h# d- _. i! ?3 T4 u3 H
  •                                                                
    - l2 ?9 j; I1 q4 e# s- e4 f
  •     解决方案:                                                               
    1 {9 Z% t$ {( G" B
  •                                                                 当多个线程当需要改变相同的值时,需要同步访问的锁定机制。如果没有两个或两个以上的线程它可能同时写入相同的值,导致内存损坏,通常导致崩溃。
      I" ?8 T% B! n. c
  • 原子包为同步访问提供了一种快速简单的方法。它是计数器最快的同步方法。它有一种定义明确用例的方法,如增加、减少、交换等。
    ) F- t  h+ ?9 u0 p
  • 同步包提供了一个更复杂的同步访问值,如地图、切片、阵列或组。您可以将其用于未存在atomic 中定义的用例。
      K5 s" b: c- X( Z, k, J
  • 在任何情况下,只有在写入时才需要锁定。*在没有锁定机制的情况下,可以安全地读取相同的值。
    ! x1 f7 a: y5 u3 @( A
  • 让我们看看您提供的代码。
    . ^) h  v  P; p& u; |0 [6 M
  • [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)}+ a8 E# ~( ~7 Y5 ?6 a. \
(注:我删除了平均值,因为它没有在原始示例中使用。+ \' g8 \' ]! U6 Z
现在,假设你不希望你的计数器提前确定。在这种情况下,您需要斥锁来同步访问。
( F, l  m, B6 F; m/ `2 I3 n8 ]让我们只用一个Mutex试试吧。这很简单,因为一次只有一个线程可以持有Lock。假如第二线程在试图锁定第一个版本之前,他们等待(或块)**那时。
9 y' G5 F" a8 [; y3 K* f1 ^
    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)}7 H8 ?' O. G1 D% [! e! c. b
上述代码可以正常工作。但是有两个问题。
/ ~; H2 {1 I  |; A[ol]如果 Lock() 和 Unlock() 如果有恐慌,即使你想从恐慌中恢复,互斥也会永远锁定。这个代码可能不会恐慌,但一般来说,假设可能更好。
! k7 c& l% P: R0 v: w获取计数器时获得排他锁。一次只有一个线程*它可以从计数器中读取。[/ol]问题#1 易于解决。延迟使用:
, K) b4 _  b* a( Z2 E. u
    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)}' p+ e2 ~# O4 _3 u7 Y
这样可以保证始终调用 Unlock()。如果由于某种原因你有不止一个返回,你只需要在函数的开头指定 Unlock() 一次。" i) O9 l# K7 T
问题#2 可以用RWMutex解决。它到底是怎么工作的,为什么有用?* J! F, o8 U. S' b3 @
RWMutex是Mutex扩展增加了两种方法:RLock和RUnlock。关于RWMutex需要注意的几点:
1 N' U. f' m6 `0 R, g$ zRLock是共享读锁。当锁被拿走时,其他线程也可以用RLock拿走自己的锁。这意味着多个线程可同时读取。它是半排他性的。% m  x7 E: T; Q2 U* U) N( P7 y& Z
互斥锁被读取锁定的,对Lock停止调用**。如果一个或多个读者持有一个锁,你就不能写了。8 J" W8 A7 W# R% V9 [! {. x, u
如果互斥锁被写锁(使用)Lock),RLock将阻塞**。
一个好的思维方式是RWMutex带读取器计数器的互斥锁。RLock和RUnlock减少它。只要计数器 > 0,对Lock调用会堵塞。
; E: W( W2 q! l( e3 |你可能会想:如果我的应用程序被大量读取,这是否意味着写入器可能被无限期阻塞?RWMutex还有一个有用的属性:
1 x1 D2 W! O; d3 P/ S如果读者计数器 > 0 并且Lock调用,以后对RLock 的调用也会被阻止,直到现有读者释放他们的锁,作者得到他的锁并稍后释放它。把它想象成杂货店收银台上方的灯,上面显示收银员是否营业。排队的人可以留在那里,他们会得到帮助,但新人不能排队。一旦最后剩下的顾客得到帮助,收银员就会休息,并且该收银机要么保持关闭,直到他们回来,要么被另一个收银员取代。
2 H# g+ t2 z; i, F4 ~8 Q3 x让我们用RWMutex修改前面的例子:* I9 F9 u/ F9 }
    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}
    ( n& ~& }: W! j1 [9 @+ M) C9 |" v
使用上述代码,我将逻辑分离为getCounter和initCounter函数:
: N4 v) G. {% T- V% n2 S  ^保持代码简单易懂。在同一函数中使用 RLock() 和 Lock() 会很难。
/ f$ o# f2 _0 N& U, ~使用 defer 尽早释放锁。
与Mutex不同的示例允许您同时添加不同的计数器。
, [& G3 n: P3 d; ?我想指出的另一件事是映射map[string]*int64包含指向计数器的指针,而不是计数器本身。如果要计数器存储在地图中map[string]int64不需要使用atomic 的Mutex。代码如下:
; M- X8 X  i; N& e% q) v2 @
    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]}& x* g8 J8 z  Y0 i1 M
你可能想这样做来减少收集 - 但这只在你有数千个计数器的时候才重要- 即便如此,计数器本身也不会占用很多空间(与字节缓冲区等东西相比)。
, @" E0 w$ u8 j; x( H% j8 c$ N+ B*当我说线程时,我的意思是 go-routine。其他语言中的线程是一种同时运行一组或多组代码的机制。创建和拆除线程的成本很高。go-routine 是基于线程的,但会重用。go-routine 休眠时,底线可以是另一个 go-routine 使用。当一个 go-routine 醒来时,它可能在不同的线程上。Go 在幕后处理一切。– 但是,当涉及到内存访问时,出于所有的意图和目的,你会 go-routine 视为线程。然而, 正在使用go-routines 不必像使用线程那样保守。
* X, E' V/ U9 Y; R, R! H* f+ q**当 go-routine 被Lock、RLock、通道或Sleep当堵塞时,底层线程可能会被重用。go-routine 不使用 CPU - 将其视为排队等候。像其他语言一样,无限循环for {}同时保持 cpu 和 go-routine 忙碌
分享到:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则