Golang:节流(延时)功能在goroutine中不起作用(在主线程中工作正常)

时间:2015-02-06 02:24:00

标签: time go goroutine

所以我正在编写一个实用程序来查询工作中的API,并且它们每10秒调整一次20个调用。很简单,我只是将我的呼叫限制在自上次呼叫以来至少0.5秒。 My Throttle实用程序工作正常,直到我尝试使用goroutines。

现在我正在使用struct / method组合:

func (c *CTKAPI) Throttle() {
if c.Debug{fmt.Println("\t\t\tEntering Throttle()")}
for { //in case something else makes a call while we're sleeping, we need to re-check
    if t := time.Now().Sub(c.LastCallTime); t < c.ThrottleTime {
        if c.Debug{fmt.Printf("\t\t\tThrottle: Sleeping %v\n", c.ThrottleTime - t)}
        time.Sleep(c.ThrottleTime - t)
    } else {
        if c.Debug{fmt.Println("\t\t\tThrottle: Released.")}
        break
    }
}
c.LastCallTime = time.Now()
if c.Debug{fmt.Println("\t\t\tExiting Throttle()")}

}

然后我在每个goroutine的每次通话之前打电话给任何人.Throttle()以确保我在开始下一次通话之前至少等了半秒钟。

但这似乎是不可靠的,并给出了不可预测的结果。是否有一种更优雅的方式来限制并发请求?

-Mike

4 个答案:

答案 0 :(得分:1)

由于您正在引入数据竞争,因此多个例程正在访问/更改c.LastCallTime。

您使用time.Tick代替c.LastCallTime int64 c.LastCallTime = time.Now().Unix()并使用atomic.LoadInt64/StoreInt64进行检查。

答案 1 :(得分:1)

实际上有一种更简单的方法:create a time ticker

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    rateLimit := time.Tick(500 * time.Millisecond)
    <-rateLimit

    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            <-rateLimit
            fmt.Println("Hello", i)
            wg.Done()
        }(i)
    }
    wg.Wait()
}

答案 2 :(得分:0)

回答我自己的问题,因为我昨晚发生了一些解决方案。    对我来说最简单的答案是使用sync.Mutex变量锁定和解锁油门功能,以确保我不会意外地同时击中它。另一种选择是将我的限制服务移动到其自己的goroutine功能中(从而消除并发调用)并与通道进行节流/确认通信,但对于此应用程序,Mutex是一个更清洁的解决方案。    以下是寻找类似解决方案的工作代码的简化版本:

package main

import (
    "fmt"
    "time"
    "sync"
)

type tStruct struct {
    delay time.Duration
    last time.Time
    lock sync.Mutex //this will be our locking variable
}

func (t *tStruct) createT() *tStruct {
    return &tStruct {
        delay: 500*time.Millisecond,
        last: time.Now(),
    }
}

func (t *tStruct) throttle(th int) {
    //we lock our function, and any other routine calling this function will block.
    t.lock.Lock()
    //and we'll defer an unlock, so when we exit the throttle, we'll be ready for another call.
    defer t.lock.Unlock()
    fmt.Printf("\tThread %v Entering Throttle Check.\n", th)
    defer fmt.Printf("\tThread %v Leaving Throttle Check.\n", th)
    for {
        p := time.Now().Sub(t.last)
        if p < t.delay {
            fmt.Printf("\tThread %v Sleeping %v.\n", th, t.delay-p)
            time.Sleep(t.delay-p)
        } else {
            fmt.Printf("\tThread %v No longer Throttled.\n", th)
            t.last = time.Now()
            break
        }
    }
}

func (t *tStruct) worker(rch <-chan string, sch chan<- string, th int) {
    fmt.Printf("Thread %v starting up.\n", th)
    defer fmt.Printf("Thread %v Dead.\n", th)
    sch <-"READY"
    for {
        r := <-rch
        fmt.Printf("Thread %v received %v\n", th, r)
        switch r {
            case "STOP":
                fmt.Printf("Thread %v returning.\n", th)
                sch <-"QUITTING"
                return
            default:
                fmt.Printf("Thread %v processing %v.\n", th, r)
                t.throttle(th)
                fmt.Printf("Thread %v done with %v.\n", th, r)
                sch <-"OK"
        }
    }
}

func main() {
    ts := tStruct{}
    ts.delay = 500*time.Millisecond
    ts.last = time.Now()
    sch := make(chan string)
    rch := make(chan string)
    tC := 3
    tA := 0

    fmt.Println("Starting Threads")
    for i:=1; i<(tC+1); i++ {
        go ts.worker(sch, rch, i)
        r := <-rch
        if r=="READY" {
            tA++
        } else {
            fmt.Println("ERROR not READY")
        }
    }

    fmt.Println("Feeding All Threads")
    for i:=1; i<(tC+1); i++ {
        sch <- "WORK"
    }

    fmt.Println("Listening for threads")
    for tA > 0{
        r := <-rch
        switch r {
            case "QUITTING":
                tA--
                fmt.Println("main received QUITTING")
                continue
            case "OK":
                fmt.Println("main received OK")
                sch <-"STOP"
                continue
            default:
                fmt.Println("Shouldn't be here!!!")
        }
    }
}

答案 3 :(得分:0)

您的新代码更好。正如另一个答案中所提到的,你有一场比赛。 Go有一个内置的竞赛检测器go build -race。这是一个了不起的工具,可以通过良好的单元测试找到适合自己的比赛。

我相信你最初的一个假设是有缺陷的。通过调整所有API调用,您可以消除任何爆发的机会。在您的方案中,每个API调用都会延迟命中,即使它可能不需要。除非你确定每个API调用都能达到节制,否则有更好的方法。

启动time.NewTicker到10秒并将计数器初始化为0.增加每个API请求的计数器。如果计数器达到20睡眠goroutines,直到计时器熄灭。当计时器熄灭时,重置计数器并继续睡觉的goroutines。

我一直想编写一个API速率限制器,所以我编写了它,你可以在这里看到它: https://github.com/tildeleb/limiter/blob/master/limiter.go

除了这个例子,它没有经过测试。任何反馈,请在github上创建一个问题。