高度な並行アプリケーションにグローバルカウンターを実装する最良の方法は何ですか?私の場合、「作業」を実行する10K〜20Kのルーチンを実行している可能性があり、ルーチンがまとめて作業しているアイテムの数とタイプをカウントしたいのですが...
「クラシック」同期コーディングスタイルは次のようになります。
var work_counter int
func GoWorkerRoutine() {
for {
// do work
atomic.AddInt32(&work_counter,1)
}
}
実行中の作業の「タイプ」を追跡したいので、これはより複雑になります。実際、次のようなものが必要になります。
var work_counter map[string]int
var work_mux sync.Mutex
func GoWorkerRoutine() {
for {
// do work
work_mux.Lock()
work_counter["type1"]++
work_mux.Unlock()
}
}
チャネルまたはこれに類似したものを使用して、「go」に最適化された方法があるはずです。
var work_counter int
var work_chan chan int // make() called somewhere else (buffered)
// started somewher else
func GoCounterRoutine() {
for {
select {
case c := <- work_chan:
work_counter += c
break
}
}
}
func GoWorkerRoutine() {
for {
// do work
work_chan <- 1
}
}
この最後の例にはまだマップがありませんが、追加するのは簡単です。このスタイルは、単純なアトミック増分よりも優れたパフォーマンスを提供しますか?グローバル値への同時アクセスとI/Oでブロックする可能性のあるものとの同時アクセスについて話しているとき、これが多少複雑かどうかはわかりません...
考えは大歓迎です。
アップデート5/28/2013:
いくつかの実装をテストしましたが、結果は期待したものではありませんでした。カウンターソースコードを次に示します。
package helpers
import (
)
type CounterIncrementStruct struct {
bucket string
value int
}
type CounterQueryStruct struct {
bucket string
channel chan int
}
var counter map[string]int
var counterIncrementChan chan CounterIncrementStruct
var counterQueryChan chan CounterQueryStruct
var counterListChan chan chan map[string]int
func CounterInitialize() {
counter = make(map[string]int)
counterIncrementChan = make(chan CounterIncrementStruct,0)
counterQueryChan = make(chan CounterQueryStruct,100)
counterListChan = make(chan chan map[string]int,100)
go goCounterWriter()
}
func goCounterWriter() {
for {
select {
case ci := <- counterIncrementChan:
if len(ci.bucket)==0 { return }
counter[ci.bucket]+=ci.value
break
case cq := <- counterQueryChan:
val,found:=counter[cq.bucket]
if found {
cq.channel <- val
} else {
cq.channel <- -1
}
break
case cl := <- counterListChan:
nm := make(map[string]int)
for k, v := range counter {
nm[k] = v
}
cl <- nm
break
}
}
}
func CounterIncrement(bucket string, counter int) {
if len(bucket)==0 || counter==0 { return }
counterIncrementChan <- CounterIncrementStruct{bucket,counter}
}
func CounterQuery(bucket string) int {
if len(bucket)==0 { return -1 }
reply := make(chan int)
counterQueryChan <- CounterQueryStruct{bucket,reply}
return <- reply
}
func CounterList() map[string]int {
reply := make(chan map[string]int)
counterListChan <- reply
return <- reply
}
論理的に思われる書き込みと読み取りの両方にチャネルを使用します。
私のテストケースは次のとおりです。
func bcRoutine(b *testing.B,e chan bool) {
for i := 0; i < b.N; i++ {
CounterIncrement("abc123",5)
CounterIncrement("def456",5)
CounterIncrement("ghi789",5)
CounterIncrement("abc123",5)
CounterIncrement("def456",5)
CounterIncrement("ghi789",5)
}
e<-true
}
func BenchmarkChannels(b *testing.B) {
b.StopTimer()
CounterInitialize()
e:=make(chan bool)
b.StartTimer()
go bcRoutine(b,e)
go bcRoutine(b,e)
go bcRoutine(b,e)
go bcRoutine(b,e)
go bcRoutine(b,e)
<-e
<-e
<-e
<-e
<-e
}
var mux sync.Mutex
var m map[string]int
func bmIncrement(bucket string, value int) {
mux.Lock()
m[bucket]+=value
mux.Unlock()
}
func bmRoutine(b *testing.B,e chan bool) {
for i := 0; i < b.N; i++ {
bmIncrement("abc123",5)
bmIncrement("def456",5)
bmIncrement("ghi789",5)
bmIncrement("abc123",5)
bmIncrement("def456",5)
bmIncrement("ghi789",5)
}
e<-true
}
func BenchmarkMutex(b *testing.B) {
b.StopTimer()
m=make(map[string]int)
e:=make(chan bool)
b.StartTimer()
for i := 0; i < b.N; i++ {
bmIncrement("abc123",5)
bmIncrement("def456",5)
bmIncrement("ghi789",5)
bmIncrement("abc123",5)
bmIncrement("def456",5)
bmIncrement("ghi789",5)
}
go bmRoutine(b,e)
go bmRoutine(b,e)
go bmRoutine(b,e)
go bmRoutine(b,e)
go bmRoutine(b,e)
<-e
<-e
<-e
<-e
<-e
}
マップの周りにミューテックスだけを使用して(書き込みをテストするだけで)シンプルなベンチマークを実装し、5つのゴルーチンを並行して実行して両方をベンチマークしました。結果は次のとおりです。
$ go test --bench=. helpers
PASS
BenchmarkChannels 100000 15560 ns/op
BenchmarkMutex 1000000 2669 ns/op
ok helpers 4.452s
ミューテックスがそれほど速くなるとは思っていませんでした...
さらなる考え?
リンクページから sync/atomic -を使用しないでください
パッケージatomicは、同期アルゴリズムの実装に役立つ低レベルのアトミックメモリプリミティブを提供します。これらの機能を正しく使用するには細心の注意が必要です。特別な低レベルのアプリケーションを除き、同期はチャネルまたは同期パッケージの機能を使用してより適切に行われます
前回はこれをしなければなりませんでした ミューテックスを使用した2番目の例のようなものと、チャネルを使用した3番目の例のようなものをベンチマークしました。物事が本当に忙しくなったときにチャネルコードが勝ちましたが、チャネルバッファを大きくするようにしてください。
ワーカーのプールを同期しようとしている場合(たとえば、n個のgoroutinesがある程度の作業を処理できるようにする場合)、チャネルはそれを実行するための非常に良い方法ですが、実際に必要なのはカウンター(たとえば、ページビュー)その後、彼らは過剰です。 sync および sync/atomic パッケージが役立ちます。
import "sync/atomic"
type count32 int32
func (c *count32) increment() int32 {
return atomic.AddInt32((*int32)(c), 1)
}
func (c *count32) get() int32 {
return atomic.LoadInt32((*int32)(c))
}
ミューテックスとロックを使用することを恐れないでください。それらが「適切なGo」ではないと考えるからです。 2番目の例では、何が起こっているかが完全に明確であり、それは非常に重要です。ミューテックスの満足度を確認し、複雑さを追加するとパフォーマンスが向上するかどうかを確認するには、自分で試してみる必要があります。
パフォーマンスの向上が必要な場合は、おそらくシャーディングが最善の方法です: http://play.golang.org/p/uLirjskGeN
欠点は、シャーディングが決定したとおりにカウントが最新になることです。 time.Since()
を呼び出すとパフォーマンスが低下する場合もありますが、いつものように、最初に測定してください:)
同期/アトミックを使用する他の答えは、ページカウンターなどに適していますが、外部APIに一意の識別子を送信することには適していません。これを行うには、CASループとしてのみ実装可能な「インクリメントアンドリターン」操作が必要です。
一意のメッセージIDを生成するint32のCASループは次のとおりです。
import "sync/atomic"
type UniqueID struct {
counter int32
}
func (c *UniqueID) Get() int32 {
for {
val := atomic.LoadInt32(&c.counter)
if atomic.CompareAndSwapInt32(&c.counter, val, val+1) {
return val
}
}
}
使用するには、次のようにします。
requestID := client.msgID.Get()
form.Set("id", requestID)
これは、多くの余分なアイドルリソースを必要としないという点でチャネルよりも有利です。既存のゴルーチンは、プログラムが必要とするすべてのカウンターに1つのゴルーチンを使用するのではなく、IDを要求するために使用されます。
TODO:チャネルに対するベンチマーク。このコードは単にレースを勝ち取ろうとスピンしている間にキューイングしているので、チャネルは競合なしの場合はより悪く、競合の多い場合はより良いと推測します。
古い質問ですが、私はこれにつまずいただけで助けになるかもしれません: https://github.com/uber-go/atomic
基本的に、Uberのエンジニアは、sync/atomic
パッケージ
私はまだ実稼働環境でこれをテストしていませんが、コードベースは非常に小さく、 ほとんどの機能 の実装はかなり ストック標準 です
チャネルまたは基本的なミューテックスを使用するよりも間違いなく優先
最後のものは近かった:
package main
import "fmt"
func main() {
ch := make(chan int, 3)
go GoCounterRoutine(ch)
go GoWorkerRoutine(1, ch)
// not run as goroutine because mein() would just end
GoWorkerRoutine(2, ch)
}
// started somewhere else
func GoCounterRoutine(ch chan int) {
counter := 0
for {
ch <- counter
counter += 1
}
}
func GoWorkerRoutine(n int, ch chan int) {
var seq int
for seq := range ch {
// do work:
fmt.Println(n, seq)
}
}
これにより、単一障害点が発生します。カウンターゴルーチンが停止すると、すべてが失われます。これは、すべてのゴルーチンが1台のコンピューターで実行されている場合は問題にならない場合がありますが、ネットワークに散在している場合には問題になる可能性があります。クラスター内の単一ノードの障害に対してカウンターを無効にするには、 特殊アルゴリズム を使用する必要があります。
これを「最も簡単な方法」であるため、これを処理するのに最適な方法と思われるシンプルなマップ+ミューテックスでこれを実装しました(Goがロックとチャネルを選択するために使用する方法です)。
package main
import (
"fmt"
"sync"
)
type single struct {
mu sync.Mutex
values map[string]int64
}
var counters = single{
values: make(map[string]int64),
}
func (s *single) Get(key string) int64 {
s.mu.Lock()
defer s.mu.Unlock()
return s.values[key]
}
func (s *single) Incr(key string) int64 {
s.mu.Lock()
defer s.mu.Unlock()
s.values[key]++
return s.values[key]
}
func main() {
fmt.Println(counters.Incr("bar"))
fmt.Println(counters.Incr("bar"))
fmt.Println(counters.Incr("bar"))
fmt.Println(counters.Get("foo"))
fmt.Println(counters.Get("bar"))
}
https://play.golang.org/p/9bDMDLFBAY でコードを実行できます。シンプルなパッケージバージョンを作成しました Gist.github.com