为高度并发应用程序实现全局计数器的最佳方法?

时间:2013-05-28 03:07:52

标签: concurrency go

为高度并发的应用程序实现全局计数器的最佳方法是什么?在我的情况下,我可能有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完成的事情时,我无法判断这或多或少是复杂的......

我们对此表示赞赏。

2013年5月28日更新:

我测试了几个实现,结果不是我的预期,这是我的计数器源代码:

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个goroutine进行基准测试。结果如下:

$ go test --bench=. helpers
PASS
BenchmarkChannels         100000             15560 ns/op
BenchmarkMutex   1000000              2669 ns/op
ok      helpers 4.452s

我不会期望互斥体会那么快......

进一步的想法?

10 个答案:

答案 0 :(得分:18)

请勿使用链接页面中的sync/atomic -

  

Package atomic提供了有用的低级原子内存原语   实现同步算法。   这些功能需要非常小心才能正确使用。除了   特殊的低级应用程序,同步性更好   渠道或同步包的设施

Last time I had to do this我使用互斥锁对您的第二个示例进行了基准测试,这看起来像是您使用频道的第三个示例。当事情变得非常繁忙时,频道代码赢了,但要确保你的频道缓冲区很大。

答案 1 :(得分:16)

如果你正在尝试同步一群工人(例如允许n goroutines来处理一些工作量),那么渠道是一个非常好的方法,但如果你真正需要的只是一个柜台(例如页面浏览量)然后他们是矫枉过正的。 syncsync/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 Playground Example

答案 2 :(得分:8)

不要因为你认为他们“不适合Go”而害怕使用互斥锁和锁定。在你的第二个例子中,它绝对清楚发生了什么,这很重要。您必须自己尝试一下,看看互斥体是多么满足,以及添加并发症是否会提高性能。

如果您确实需要提高性能,也许分片是最好的方法: http://play.golang.org/p/uLirjskGeN

缺点是你的计数只会与你的分片决定一样最新。通过调用time.Since()可能会有很多性能点击,但是,一如既往地先测量它:)

答案 3 :(得分:4)

使用sync / atomic的另一个答案适用于页面计数器之类的内容,但不适用于向外部API提交唯一标识符。要做到这一点,你需要一个&#34;增量和返回&#34;操作,只能作为CAS循环实现。

这是一个围绕int32的CAS循环,用于生成唯一的消息ID:

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)

这比渠道更有优势,因为它不需要尽可能多的额外空闲资源 - 现有的goroutine用于请求ID,而不是为你的程序需要的每个计数器使用一个goroutine。

TODO:对抗频道的基准。我猜测在无竞争情况下通道更糟糕,在高争用情况下更好,因为他们排队时这个代码只是试图赢得比赛。

答案 4 :(得分:3)

古老的问题,但是我偶然发现了这个问题,它可能会有所帮助:https://github.com/uber-go/atomic

基本上,Uber的工程师已经在sync/atomic包的顶部构建了一些不错的util函数

我尚未在生产环境中对此进行测试,但是代码库非常小,most functions的实现相当stock standard

绝对优于使用通道或基本互斥体

答案 5 :(得分:2)

最后一个是关闭的:

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)
    }
}

这引入了单点故障:如果计数器goroutine死亡,一切都会丢失。如果所有goroutine都在一台计算机上执行,这可能不是问题,但如果它们分散在网络上可能会成为问题。为了使计数器免受群集中单个节点的故障,必须使用special algorithms

答案 6 :(得分:2)

我用一个简单的map + mutex实现了这个,这似乎是处理这个的最好方法,因为它是最简单的方法&#34; (这就是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上运行代码。 我制作了一个简单的打包版本on gist.github.com

答案 7 :(得分:1)

如果您的工作计数器类型不是动态的,即您可以预先将它们全部写出来,我认为您不会比这更简单或更快。

没有互斥锁、没有通道、没有映射。只是一个静态大小的数组和一个枚举。

type WorkType int

const (
    WorkType1 WorkType = iota
    WorkType2
    WorkType3
    WorkType4
    NumWorkTypes
)

var workCounter [NumWorkTypes]int64

func updateWorkCount(workType WorkType, delta int) {
    atomic.AddInt64(&workCounter[workType], int64(delta))
}

用法如下:

updateWorkCount(WorkType1, 1)

如果您有时需要将工作类型作为字符串处理以用于显示目的,您始终可以使用 stringer 之类的工具生成代码

答案 8 :(得分:0)

请注意,没有理由不能只将atomic与地图一起使用。

一个技巧是,映射必须已经包含类型,否则work_counter[type]意味着要写。因此,在创建任何go例程之前,请确保初始化所有计数器。

var work_counter map[string]int
var work_mux sync.Mutex

func initializeCounter(type string) {
    work_counter[type] = 0
}

func GoWorkerRoutine(type string) {
    for {
        // do work
        atomic.AddInt32(&work_counter[type], 1)
    }    
}

唯一的缺点是必须初始化所有类型,并且除非您具有适当的互斥保护或首先停止所有go例程,否则其他go例程都不能向该work_counter映射添加更多类型。但是,如果您需要更快的速度,这可能是最好的解决方案。 (当然,如果确实需要极高的速度,则可以使用其中每个类型都是整数而不是字符串的数组)。

按照您的建议使用频道也是可以的。实际上,您可以对增量进行序列化,因此您可能会觉得不需要互斥体,但是在编写时:

work_chan <- 1

Golang将值1添加到需要引擎盖下的互斥量的通道(以及从通道检索值时再次)。因此,从速度来看,它肯定要慢一些。

答案 9 :(得分:0)

一个人看看,让我知道你的想法。

src / test / helpers / helpers.go

package helpers

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
}

src / test / distributed / distributed.go

package distributed

type Counter struct {
    buckets map[string]int
    incrQ   chan incrQ
    readQ   chan readQ
    sumQ    chan chan int
}

func New() Counter {
    c := Counter{
        buckets: make(map[string]int, 100),
        incrQ:   make(chan incrQ, 1000),
        readQ:   make(chan readQ, 0),
        sumQ:    make(chan chan int, 0),
    }
    go c.run()
    return c
}

func (c Counter) run() {
    for {
        select {
        case a := <-c.readQ:
            a.res <- c.buckets[a.bucket]
        case a := <-c.sumQ:
            var sum int
            for _, cnt := range c.buckets {
                sum += cnt
            }
            a <- sum
        case a := <-c.incrQ:
            c.buckets[a.bucket] += a.count
        }
    }
}

func (c Counter) Get(bucket string) int {
    res := make(chan int)
    c.readQ <- readQ{bucket: bucket, res: res}
    return <-res
}

func (c Counter) Sum() int {
    res := make(chan int)
    c.sumQ <- res
    return <-res
}

type readQ struct {
    bucket string
    res    chan int
}

type incrQ struct {
    bucket string
    count  int
}

func (c Counter) Agent(bucket string, limit int) *Agent {
    a := &Agent{
        bucket:   bucket,
        limit:    limit,
        sendIncr: c.incrQ,
    }
    return a
}

type Agent struct {
    bucket   string
    limit    int
    count    int
    sendIncr chan incrQ
}

func (a *Agent) Incr(n int) {
    a.count += n
    if a.count > a.limit {
        select {
        case a.sendIncr <- incrQ{bucket: a.bucket, count: a.count}:
            a.count = 0
        default:
        }
    }
}

func (a *Agent) Done() {
    a.sendIncr <- incrQ{bucket: a.bucket, count: a.count}
    a.count = 0
}

src / test / helpers_test.go

package counters

import (
    "sync"
    "testing"
)

var mux sync.Mutex
var m map[string]int

func bmIncrement(bucket string, value int) {
    mux.Lock()
    m[bucket] += value
    mux.Unlock()
}

func BenchmarkMutex(b *testing.B) {
    b.StopTimer()
    m = make(map[string]int)
    buckets := []string{
        "abc123",
        "def456",
        "ghi789",
    }
    b.StartTimer()

    var wg sync.WaitGroup
    wg.Add(b.N)
    for i := 0; i < b.N; i++ {
        go func() {
            for _, b := range buckets {
                bmIncrement(b, 5)
            }
            for _, b := range buckets {
                bmIncrement(b, 5)
            }
            wg.Done()
        }()
    }
    wg.Wait()
}

src / test / distributed_test.go

package counters

import (
    "sync"
    "test/counters/distributed"
    "testing"
)

func BenchmarkDistributed(b *testing.B) {
    b.StopTimer()
    counter := distributed.New()
    agents := []*distributed.Agent{
        counter.Agent("abc123", 100),
        counter.Agent("def456", 100),
        counter.Agent("ghi789", 100),
    }
    b.StartTimer()

    var wg sync.WaitGroup
    wg.Add(b.N)
    for i := 0; i < b.N; i++ {
        go func() {
            for _, a := range agents {
                a.Incr(5)
            }
            for _, a := range agents {
                a.Incr(5)
            }
            wg.Done()
        }()
    }
    for _, a := range agents {
        a.Done()
    }
    wg.Wait()
}

结果

$ go test --bench=. --count 10 -benchmem
goos: linux
goarch: amd64
pkg: test/counters
BenchmarkDistributed-4       3356620           351 ns/op          24 B/op          0 allocs/op
BenchmarkDistributed-4       3414073           368 ns/op          11 B/op          0 allocs/op
BenchmarkDistributed-4       3371878           374 ns/op           7 B/op          0 allocs/op
BenchmarkDistributed-4       3240631           387 ns/op           3 B/op          0 allocs/op
BenchmarkDistributed-4       3169230           389 ns/op           2 B/op          0 allocs/op
BenchmarkDistributed-4       3177606           386 ns/op           0 B/op          0 allocs/op
BenchmarkDistributed-4       3064552           390 ns/op           0 B/op          0 allocs/op
BenchmarkDistributed-4       3065877           409 ns/op           2 B/op          0 allocs/op
BenchmarkDistributed-4       2924686           400 ns/op           1 B/op          0 allocs/op
BenchmarkDistributed-4       3049873           389 ns/op           0 B/op          0 allocs/op
BenchmarkMutex-4             1000000          1106 ns/op          17 B/op          0 allocs/op
BenchmarkMutex-4              948331          1246 ns/op           9 B/op          0 allocs/op
BenchmarkMutex-4             1000000          1244 ns/op          12 B/op          0 allocs/op
BenchmarkMutex-4             1000000          1246 ns/op          11 B/op          0 allocs/op
BenchmarkMutex-4             1000000          1228 ns/op           1 B/op          0 allocs/op
BenchmarkMutex-4             1000000          1235 ns/op           2 B/op          0 allocs/op
BenchmarkMutex-4             1000000          1244 ns/op           1 B/op          0 allocs/op
BenchmarkMutex-4             1000000          1214 ns/op           0 B/op          0 allocs/op
BenchmarkMutex-4              956024          1233 ns/op           0 B/op          0 allocs/op
BenchmarkMutex-4             1000000          1213 ns/op           0 B/op          0 allocs/op
PASS
ok      test/counters   37.461s

如果将极限值更改为1000,则代码会变得更快,无需担心

$ go test --bench=. --count 10 -benchmem
goos: linux
goarch: amd64
pkg: test/counters
BenchmarkDistributed-4       5463523           221 ns/op           0 B/op          0 allocs/op
BenchmarkDistributed-4       5455981           220 ns/op           0 B/op          0 allocs/op
BenchmarkDistributed-4       5591240           213 ns/op           0 B/op          0 allocs/op
BenchmarkDistributed-4       5277915           212 ns/op           0 B/op          0 allocs/op
BenchmarkDistributed-4       5430421           213 ns/op           0 B/op          0 allocs/op
BenchmarkDistributed-4       5374153           226 ns/op           0 B/op          0 allocs/op
BenchmarkDistributed-4       5656743           219 ns/op           0 B/op          0 allocs/op
BenchmarkDistributed-4       5337343           211 ns/op           0 B/op          0 allocs/op
BenchmarkDistributed-4       5353845           217 ns/op           0 B/op          0 allocs/op
BenchmarkDistributed-4       5416137           217 ns/op           0 B/op          0 allocs/op
BenchmarkMutex-4             1000000          1002 ns/op         135 B/op          0 allocs/op
BenchmarkMutex-4             1253211          1141 ns/op          58 B/op          0 allocs/op
BenchmarkMutex-4             1000000          1261 ns/op           3 B/op          0 allocs/op
BenchmarkMutex-4              987345          1678 ns/op          59 B/op          0 allocs/op
BenchmarkMutex-4              925371          1247 ns/op           0 B/op          0 allocs/op
BenchmarkMutex-4             1000000          1259 ns/op           2 B/op          0 allocs/op
BenchmarkMutex-4              978800          1248 ns/op           0 B/op          0 allocs/op
BenchmarkMutex-4              982144          1213 ns/op           0 B/op          0 allocs/op
BenchmarkMutex-4              975681          1254 ns/op           0 B/op          0 allocs/op
BenchmarkMutex-4              994789          1205 ns/op           0 B/op          0 allocs/op
PASS
ok      test/counters   34.314s

更改Counter.incrQ的长度也将极大地影响性能,尽管它会占用更多内存。