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

Concurrency 为高并发应用程序实现全局计数器的最佳方法?,concurrency,go,Concurrency,Go,对于高度并发的应用程序,实现全局计数器的最佳方法是什么?在我的例子中,我可能有10K-20K go例程执行“工作”,我想计算这些例程共同工作的项目的数量和类型 “经典”同步编码风格如下所示: var work_counter int func GoWorkerRoutine() { for { // do work atomic.AddInt32(&work_counter,1) } } 现在这变得更复杂了,因为我想跟踪正在完成

对于高度并发的应用程序,实现全局计数器的最佳方法是什么?在我的例子中,我可能有10K-20K go例程执行“工作”,我想计算这些例程共同工作的项目的数量和类型

“经典”同步编码风格如下所示:

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
    }    
}
我没想到互斥锁会快那么多


进一步思考?

最后一个想法很接近:

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)
    }
}
主程序包
输入“fmt”
func main(){
ch:=制造(成交量,3)
go go反例行程序(ch)
go GoWorkerRoutine(1,ch)
//不作为goroutine运行,因为mein()将刚刚结束
GoWorkerRoutine(2,ch)
}
//从别的地方开始
func GO计数器例程(ch chan int){
计数器:=0
为了{

ch不要因为认为互斥锁和锁“不合适”而害怕使用它们。在第二个示例中,发生了什么事情是绝对清楚的,这非常重要。您必须亲自尝试,看看互斥锁有多满足,以及增加复杂性是否会提高性能

如果确实需要提高性能,切分可能是最好的方法:

缺点是,您的计数只会根据您的切分决定最新。调用
时间也可能会对性能造成影响。因为()
太多了,但一如既往,首先要测量它:)

不要使用链接页面中的-

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


我用互斥量对第二个示例进行了基准测试,用通道对第三个示例进行了基准测试。当事情变得非常繁忙时,通道代码会胜出,但请确保通道缓冲区很大。

如果您试图同步一组工作人员(例如,允许n个goroutines处理一些工作量)然后频道是一个很好的方法,但是如果你真正需要的只是一个计数器(例如页面浏览量),那么它们就太过了。和包就是为了帮助你

import "sync/atomic"

type count32 int32

func (c *count32) inc() int32 {
    return atomic.AddInt32((*int32)(c), 1)
}

func (c *count32) get() int32 {
    return atomic.LoadInt32((*int32)(c))
}

使用sync/atomic的另一个答案适用于页面计数器之类的事情,但不适用于向外部API提交唯一标识符。为此,您需要一个“增量和返回”操作,该操作只能作为CAS循环实现

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

导入“同步/原子”
类型UniqueID结构{
计数器int32
}
func(c*UniqueID)Get()int32{
为了{
val:=atomic.LoadInt32(&c.counter)
如果是原子的,则比较数据WAPINT32(&c.计数器,val,val+1){
返回值
}
}
}
要使用它,只需执行以下操作:

requestID := client.msgID.Get()
form.Set("id", requestID)
与通道相比,这有一个优势,即它不需要那么多额外的空闲资源——现有的goroutine在请求ID时使用,而不是对程序需要的每个计数器使用一个goroutine


TODO:针对通道进行基准测试。我猜通道在无争用情况下更差,在高争用情况下更好,因为它们有队列,而这段代码只是在试图赢得比赛时旋转。

我使用简单的map+互斥来实现这一点,这似乎是处理这一问题的最佳方法,因为它是“最简单的方法”(Go说用它来选择锁和通道)

您可以在上运行代码。
我做了一个简单的打包版本,这是一个老问题,但我偶然发现了这个问题,它可能会有所帮助:

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

我还没有在生产中对此进行测试,但是代码库非常小,并且的实现非常复杂


与使用通道或基本互斥量相比,您肯定更喜欢

自己看看,然后告诉我您的想法

src/test/helpers/helpers.go

包帮助程序
类型CounterIncrementStruct{
桶串
值int
}
类型计数器查询结构{
桶串
陈氏国际频道
}
变量计数器映射[string]int
var counterIncrementChan CounterIncrementStruct
var counterQueryChan chan CounterQueryStruct
var counterListChan映射[string]int
func计数器初始化(){
计数器=make(映射[string]int)
counterIncrementChan=make(chan CounterIncrementStruct,0)
counterQueryChan=make(chan CounterQueryStruct,100)
counterListChan=make(chan-chan-map[string]int,100)
goCounterWriter()
}
func goCounterWriter(){
为了{
挑选{

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

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

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)

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

互斥体是一种较低级别的原语,并且有其自己的有用位置,但它们的组合方式与CSP设计的频道使用方式不同。因此,如果有疑问,请使用频道。谢谢,频道似乎是最好的选择最后,它不会增加太多性能方面的复杂性。我只是对sync/atomic.AddInt64进行基准测试,以发现sync/atomic.AddInt64比sync.Mutex慢,因为我的问题是每个结构只有一个计数器。我认为您应该在决定使用sync/atomic或sync.Mutex之前进行基准测试。请从
sync/atomic>中找到该引用requestID := client.msgID.Get()
form.Set("id", requestID)
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"))

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