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