複数のスレッド* が同じ値を変更する必要がある場合、アクセスを同期するためにロック メカニズムが必要です。これがないと、2 つ以上のスレッド* が同時に同じ値に書き込んで、メモリが破損し、通常はクラッシュする可能性があります。
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を保持できるのは 1 つのスレッド* だけなので簡単です。最初のスレッドがUnlockでスレッドを解放する前に、2 番目のスレッド* がLockを試みた場合、それまで待機 (またはブロック) ** します。
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)
}
上記のコードは問題なく動作します。しかし、2 つの問題があります。
- Lock() と Unlock() の間にパニックが発生した場合、たとえパニックから回復したとしても、ミューテックスは永久にロックされます。このコードはおそらくパニックにはなりませんが、一般的にパニックになる可能性があると想定することをお勧めします。
- カウンタのフェッチ中に排他ロックが取得されます。一度に 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() を 1 回指定するだけで済みます。
問題 2 はRWMutexで解決できます。正確にはどのように機能し、なぜ便利なのですか?
RWMutexはMutexの拡張であり、 RLockとRUnlockの 2 つのメソッドが追加されています。RWMutexについて注意すべき重要な点がいくつかあります。
RLockは共有読み取りロックです。ロックが取得されると、他のスレッド*もRLockを使用して独自のロックを取得できます。これは、複数のスレッド*が同時に読み取ることができることを意味します。半独占です。
ミューテックスが読み取りロックされている場合、Lockの呼び出しはブロックされます**。1 つ以上のリーダーがロックを保持している場合、書き込みはできません。
ミューテックスが ( Lockで) 書き込みロックされている場合、RLockはブロックします**。
これについて考える良い方法は、 RWMutexがリーダー カウンターを備えたMutexであるということです。RLockはカウンタをインクリメントし、RUNlockはカウンタをデクリメントします。そのカウンターが > 0 である限り、Lockの呼び出しはブロックされます。
あなたは次のように考えているかもしれません: 私のアプリケーションが頻繁に読み込まれる場合、それはライターが無期限にブロックされる可能性があることを意味しますか? いいえ。 RWMutexにはもう 1 つの便利なプロパティがあります。
- リーダー カウンターが > 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
}
getCounter
上記のコードでは、ロジックとinitCounter
関数を次のように分離しました。
- コードは理解しやすいようにシンプルにします。同じ関数内で RLock() と Lock() を実行するのは困難です。
- defer を使用している間は、できるだけ早くロックを解放してください。
上記のコードは、Mutexの例とは異なり、異なるカウンターを同時にインクリメントできます。
もう 1 つ指摘しておきたいのは、上記のすべての例で、マップmap[string]*int64
にはカウンター自体ではなく、カウンターへのポインターが含まれているということです。カウンターをマップに格納する場合は、atomicなしでMutexmap[string]int64
を使用する必要があります。そのコードは次のようになります。
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 を意味します。他の言語のスレッドは、1 つ以上のコード セットを同時に実行するためのメカニズムです。スレッドの作成と破棄にはコストがかかります。go-routine はスレッドの上に構築されますが、スレッドを再利用します。go-routine がスリープ状態になると、基になるスレッドを別の go-routine で使用できます。go-routine が起動すると、別のスレッドにある可能性があります。Go はこれらすべてを舞台裏で処理します。-- しかし、すべての意図と目的のために、メモリ アクセスに関しては go-routine をスレッドのように扱います。ただし、go-routine を使用する場合は、スレッドの場合ほど保守的である必要はありません。
**
go-routine が 、 、チャネル、または Sleep によってブロックされるLock
とRLock
、基になるスレッドが再利用される可能性があります。その go-routine は CPU を使用しません - 並んで待っていると考えてください。他の言語と同様for {}
に、CPU と go-routine をビジー状態に保ちながらブロックするような無限ループは、円を描いて走り回っていると考えてください。めまいがしたり、吐き気を催したり、周りの人々はあまり幸せではありません。