同じような記事がすでにあるのでタイトルが悩みました(汗)。
「Go言語による並行処理」を読んでいて後半にgolang.org/x/time/rateというパッケージを使っていました。もう少し調べておきたいと思ったのでQiitaに投稿してみます。
TL;DR
こっちの記事の説明の方が詳しい。
golang.org/x/time/rateで速度制限を行う
でもこの記事ではタイムスタンプも入れて、もうちょっと動作をわかりやすくしてみました。
なお本記事で紹介してるサンプルコードは下記gistにあげてます。
https://gist.github.com/suganoo/2cddeae679edbbaf5f3b319338b30976
どんなもの?
rate パッケージは一定周期の処理をする時に速度制限をしてくれるパッケージです。
トークンと呼ばれる概念がありまして、一定時間ごとにトークンが溜まると処理を実行できます。そのトークンが溜まる速度を調節することで実行処理頻度を調節することができます。
例えば1秒間に5回の速度である処理を実行したいとき下記のようにします。
n := rate.Every(time.Second / time.Duration(5)) // 1秒間に5回の頻度
limiter := rate.NewLimiter(n, 1) // リミッターを作る
ctx := context.Background()
err := limiter.Wait(ctx) // 1秒間に5回トークンが溜まり実行可能になる。
Every() で実行頻度を指定し
、NewLimiterでリミッターを作り
、Wait()でトークンが溜まるまでWaitして
、実行頻度を調節することができます。
以降サンプルコードで動作を確認してみましょう。
実行時のタイムスタンプが細かくわかるようにマイクロ秒で表示してみます。
log.SetFlags(log.Lmicroseconds)
基本的な使い方
1秒間に5回処理を実行するサンプルです。
https://play.golang.org/p/SA45mo6L0hr
package main
import (
"context"
"log"
"os"
"time"
"golang.org/x/time/rate"
)
func main() {
log.SetOutput(os.Stdout)
log.SetFlags(log.Lmicroseconds)
// 1秒間に実行する回数
ntimes := 5
n := rate.Every(time.Second / time.Duration(ntimes))
// バッファーは1
limiter := rate.NewLimiter(n, 1)
ctx := context.Background()
log.Println("--- Start ---")
for i := 0; i < 20 ; i ++ {
if err := limiter.Wait(ctx); err != nil {
log.Fatalln(err)
}
log.Printf("Do work %02d", i+1)
}
}
実行結果はこうなります。
17:29:32.839459 --- Start ---
17:29:32.839559 Do work 01
17:29:33.039795 Do work 02
17:29:33.239790 Do work 03
17:29:33.439756 Do work 04
17:29:33.639779 Do work 05
17:29:33.839807 Do work 06
17:29:34.039784 Do work 07
17:29:34.239764 Do work 08
17:29:34.439718 Do work 09
17:29:34.639772 Do work 10
17:29:34.839776 Do work 11
17:29:35.039786 Do work 12
17:29:35.239792 Do work 13
17:29:35.439847 Do work 14
17:29:35.639805 Do work 15
17:29:35.839770 Do work 16
17:29:36.039750 Do work 17
17:29:36.239730 Do work 18
17:29:36.439778 Do work 19
17:29:36.639786 Do work 20
タイムスタンプを見ると1秒間に約5回処理が実行されているのがわかるかと思います。
1秒 / 5回 = 200ミリ秒なので、確かに200ミリ秒単位になってるのもわかりますね。
サンプルコードを全て書くと冗長な部分もでてくるので、以降はmain()の部分だけを書いていくことにします。
サンプルコードとしてはplaygroundや冒頭の方にあるgistを参考にしてみてください。
NewLimiterのバッファー
上のサンプルではNewLimiterのバッファーを1にしていました。
次のサンプルでは、このバッファーの動きを理解するため10にしてみます。
// バッファーを10にする
limiter := rate.NewLimiter(n, 10)
サンプルコード
https://play.golang.org/p/WEoIKXJyfh8
func main() {
log.SetOutput(os.Stdout)
log.SetFlags(log.Lmicroseconds)
// 1秒間に実行する回数
ntimes := 5
n := rate.Every(time.Second / time.Duration(ntimes))
// バッファーを10にする
limiter := rate.NewLimiter(n, 10)
ctx := context.Background()
log.Println("--- Start ---")
for i := 0; i < 20 ; i ++ {
if err := limiter.Wait(ctx); err != nil {
log.Fatalln(err)
}
log.Printf("Do work %02d", i+1)
}
}
実行結果はこうなります。
17:31:15.981971 --- Start ---
17:31:15.982024 Do work 01
17:31:15.982027 Do work 02
17:31:15.982029 Do work 03
17:31:15.982031 Do work 04
17:31:15.982033 Do work 05
17:31:15.982035 Do work 06
17:31:15.982036 Do work 07
17:31:15.982038 Do work 08
17:31:15.982040 Do work 09
17:31:15.982042 Do work 10 // <--- ここまで一気に実行されています
17:31:16.182243 Do work 11
17:31:16.382219 Do work 12
17:31:16.582234 Do work 13
17:31:16.782242 Do work 14
17:31:16.982248 Do work 15
17:31:17.182247 Do work 16
17:31:17.382214 Do work 17
17:31:17.582262 Do work 18
17:31:17.782244 Do work 19
17:31:17.982247 Do work 20
最初の一回目の処理が始まるまでにバッファーで指定した10コ分のトークンが即座に貯まったのでで、forループが始まると一気に実行されました。このようにトークンをバッファー数分貯めることができます。
そのため実行頻度以上のバッファーを指定する時は注意が必要です。
rate.Limit(1)こんな指定の仕方
Limit()を指定する場合、こんな指定方法もあるようです。
下記のように書くと1秒間に1回の頻度になるようです。
rate.Limit(1)
※なるようです.....と書いたのはドキュメントやソースコードを見てもそういう書き方が見つからなかったんですね...(汗)。「Go言語による並行処理」ではこのように書いてたので、まあできるんだろうと思います。
なんだかわからないけど、動くからいいや。あはは
https://play.golang.org/p/YVyYdBoxf1Q
func main() {
log.SetOutput(os.Stdout)
log.SetFlags(log.Lmicroseconds)
// Limit を rate.Limit(n) にすると1秒間にn回
limiter := rate.NewLimiter(rate.Limit(1), 1)
ctx := context.Background()
log.Println("--- Start ---")
for i := 0; i < 20 ; i ++ {
if err := limiter.Wait(ctx); err != nil {
log.Fatalln(err)
}
log.Printf("Do work %02d", i+1)
}
}
下記が実行結果です。
17:43:46.807694 --- Start ---
17:43:46.807780 Do work 01
17:43:47.808017 Do work 02
17:43:48.808015 Do work 03
17:43:49.807991 Do work 04
17:43:50.807969 Do work 05
17:43:51.807975 Do work 06
17:43:52.807996 Do work 07
17:43:53.807986 Do work 08
17:43:54.808001 Do work 09
17:43:55.807990 Do work 10
17:43:56.808000 Do work 11
17:43:57.808031 Do work 12
17:43:58.808018 Do work 13
17:43:59.808008 Do work 14
17:44:00.808003 Do work 15
17:44:01.807958 Do work 16
17:44:02.807953 Do work 17
17:44:03.807984 Do work 18
17:44:04.807985 Do work 19
17:44:05.807995 Do work 20
1秒ごとに実行されました。
例えばこれがrate.Limit(2)になると、1秒間に2回実行されるようになります。
WaitN()で複数トークンごとに実行
WaitN()という関数もあります。
これは指定したトークン数毎に実行させることができます。
例えば下記の例を見てください。
n := rate.Every(time.Second / time.Duration(5)) // 1秒間に5トークン
limiter := rate.NewLimiter(n, 10)
err := limiter.WaitN(ctx, 5) // 5トークンごとに1回実行
1秒間に5回実施
=1秒間に5コのトークン
となるようにEvery()で指定しています。ですが、Wait(ctx, 5) とあるように5コのトークンが溜まったら実行するようにしています。
これは、1秒間に5コのトークン
→5トークンで1回実施
、しているので、
すなわち結局1秒間に1回実行される
ことになります。
サンプルコードです
https://play.golang.org/p/A3cR3wuSllq
func main() {
log.SetOutput(os.Stdout)
log.SetFlags(log.Lmicroseconds)
// 1秒間に実行する回数
ntimes := 5
n := rate.Every(time.Second / time.Duration(ntimes))
limiter := rate.NewLimiter(n, 10)
ctx := context.Background()
log.Println("--- Start ---")
for i := 0; i < 20 ; i ++ {
// 1秒間に5コトークンが溜まるが、5トークン溜まったごとに1回実行
// →すなわち1秒に一回実行になる
if err := limiter.WaitN(ctx, 5); err != nil {
log.Fatalln(err)
}
log.Printf("Do work %02d", i+1)
}
}
実行結果を見てみましょう。
17:55:20.033969 --- Start ---
17:55:20.034020 Do work 01
17:55:20.034023 Do work 02 // <--- バッファーを10としているので 10 / 5 = 2回同時に実行されている
17:55:21.034249 Do work 03
17:55:22.034229 Do work 04
17:55:23.034255 Do work 05
17:55:24.034258 Do work 06
17:55:25.034214 Do work 07
17:55:26.034269 Do work 08
17:55:27.034190 Do work 09
17:55:28.034225 Do work 10
17:55:29.034215 Do work 11
17:55:30.034232 Do work 12
17:55:31.034231 Do work 13
17:55:32.034242 Do work 14
17:55:33.034237 Do work 15
17:55:34.034243 Do work 16
17:55:35.034243 Do work 17
17:55:36.034278 Do work 18
17:55:37.034257 Do work 19
17:55:38.034246 Do work 20
最初の2回はバッファーを10にしているので2回同時に実行されていますが、その後は1秒間に1回実行されています。
どんな時にこれが使えるのかな?っと想像してみたのですが、いくつかの処理があって割合で頻度を変えたいといったときに使えるんじゃないのかなと思います。
例えば処理A:処理B:処理Cを 5:2:3 で実行したいときにそれぞれLimiterを生成して、WaitN(ctx, n)のnの値を5,2,3に指定すれば実現できるでしょう。
実行速度を途中で変える
SetLimit()を使うと途中から実行速度を変えることができます。
下記の例では11回以降のループでは1秒間に2回実行するように変更しています。
limiter.SetLimit(rate.Every(time.Second / time.Duration(2)))
サンプルコード
https://play.golang.org/p/mKyk0fjiv51
func main() {
log.SetOutput(os.Stdout)
log.SetFlags(log.Lmicroseconds)
// 1秒間に実行する回数
ntimes := 5
n := rate.Every(time.Second / time.Duration(ntimes))
limiter := rate.NewLimiter(n, 1)
ctx := context.Background()
log.Println("--- Start ---")
for i := 0; i < 20 ; i ++ {
if err := limiter.Wait(ctx); err != nil {
log.Fatalln(err)
}
// 10回目から1秒間に2回実行に変更する
if (i + 1) == 10 {
limiter.SetLimit(rate.Every(time.Second / time.Duration(2)))
}
log.Printf("Do work %02d", i+1)
}
}
実行結果です。
18:04:21.237587 --- Start ---
18:04:21.237637 Do work 01
18:04:21.437903 Do work 02
18:04:21.637820 Do work 03
18:04:21.837844 Do work 04
18:04:22.037846 Do work 05
18:04:22.237880 Do work 06
18:04:22.437871 Do work 07
18:04:22.637830 Do work 08
18:04:22.837845 Do work 09
18:04:23.037851 Do work 10
18:04:23.537504 Do work 11 // <--- 11回目以降から実行速度が変わっています
18:04:24.037544 Do work 12
18:04:24.537508 Do work 13
18:04:25.037518 Do work 14
18:04:25.537554 Do work 15
18:04:26.037556 Do work 16
18:04:26.537548 Do work 17
18:04:27.037534 Do work 18
18:04:27.537534 Do work 19
18:04:28.037544 Do work 20
上記のように途中から実行速度が変わっていることがわかるかと思います。
次の実行までどれくらい
Reservation.Delay()では次の実行までどれくらい待つ必要があるかを教えてくれます。
rsv := limiter.Reserve()
rsv.Delay()
サンプルコードでは11回目以降にReserverポインタを取得して確認してみました。
https://play.golang.org/p/VDLNL265EG4
for i := 0; i < 20 ; i ++ {
if err := limiter.Wait(ctx); err != nil {
log.Fatalln(err)
}
log.Printf("Do work %02d", i+1)
if (i + 1) == 10 {
rsv = limiter.Reserve()
}
if rsv != nil {
log.Printf(rsv.Delay().String())
}
}
実行結果です。
18:52:56.763809 --- Start ---
18:52:56.763858 Do work 01
18:52:56.964062 Do work 02
18:52:57.164063 Do work 03
18:52:57.364088 Do work 04
18:52:57.564032 Do work 05
18:52:57.764060 Do work 06
18:52:57.964093 Do work 07
18:52:58.164063 Do work 08
18:52:58.364081 Do work 09
18:52:58.564029 Do work 10
18:52:58.564063 199.79173ms // <--- 次の実行までの時間
18:52:58.964117 Do work 11
18:52:58.964133 0s
18:52:59.164076 Do work 12
18:52:59.164093 0s
18:52:59.364049 Do work 13
18:52:59.364077 0s
18:52:59.564050 Do work 14
18:52:59.564068 0s
18:52:59.764130 Do work 15
18:52:59.764148 0s
18:52:59.964103 Do work 16
18:52:59.964120 0s
18:53:00.164049 Do work 17
18:53:00.164077 0s
18:53:00.364126 Do work 18
18:53:00.364142 0s
18:53:00.564072 Do work 19
18:53:00.564089 0s
18:53:00.764112 Do work 20
18:53:00.764131 0s
10回目の実行後に次の実行までの待ち時間が表示されています。
それ以降に0sが続くのは改めてReserverポインタを取得していないからです。
トークンが溜まったか?
Allow()は実行する前にトークンが溜まったかどうかを確認することができます。
サンプルコードとして10回目以降には実行後にsleepを入れて少し待ち時間を入れてみます。そこでAllow()がどんな挙動するか見てみます。
https://play.golang.org/p/7GyMMl0PS_L
for i := 0; i < 20 ; i ++ {
if err := limiter.Wait(ctx); err != nil {
log.Fatalln(err)
}
log.Printf("Do work %02d", i+1)
if 10 <= (i + 1) {
// sleep する
time.Sleep(200 * time.Millisecond)
}
if limiter.Allow() {
log.Println("Allow() true")
} else {
log.Println("Allow() false")
}
}
実行結果です。
19:01:50.117299 --- Start ---
19:01:50.117350 Do work 01
19:01:50.117353 Allow() false
19:01:50.317610 Do work 02
19:01:50.317629 Allow() false
19:01:50.517559 Do work 03
19:01:50.517576 Allow() false
19:01:50.717573 Do work 04
19:01:50.717592 Allow() false
19:01:50.917506 Do work 05
19:01:50.917524 Allow() false
19:01:51.117570 Do work 06
19:01:51.117588 Allow() false
19:01:51.317564 Do work 07
19:01:51.317586 Allow() false
19:01:51.517576 Do work 08
19:01:51.517595 Allow() false
19:01:51.717560 Do work 09
19:01:51.717578 Allow() false
19:01:51.917548 Do work 10
19:01:52.117767 Allow() true // <--- 待ち時間が入ってtrueになった
19:01:52.317993 Do work 11
19:01:52.518254 Allow() true
19:01:52.718454 Do work 12
19:01:52.918698 Allow() true
19:01:53.118913 Do work 13
19:01:53.319110 Allow() true
19:01:53.519305 Do work 14
19:01:53.719535 Allow() true
19:01:53.919728 Do work 15
19:01:54.119973 Allow() true
19:01:54.320157 Do work 16
19:01:54.520363 Allow() true
19:01:54.720583 Do work 17
19:01:54.920774 Allow() true
19:01:55.120989 Do work 18
19:01:55.321212 Allow() true
19:01:55.521434 Do work 19
19:01:55.721667 Allow() true
19:01:55.921878 Do work 20
19:01:56.122113 Allow() true
10回目からはWait()実行後に200ミリ秒の待ち時間を入れたためトークンが貯まるようになり、Allow() true
に変わってることがわかります。
最後に
「Go言語による並行処理」 の中で出てくるrateパッケージが便利だなーと勉強になったので、まとめておきたいと思って本記事を書きました。実際にrateパッケージを使うときには別処理をgoroutineで実行させて、goroutineの中のfor文で実行するのがベターでしょう。
また上記に挙げた例以外にも便利な関数はありますが、上記と似たような使い方をしているので参考になれば幸いです。
余談ですが「Go言語による並行処理」はめちゃいい本でとてもgoroutineの勉強になりました。サンプルコードを写経してみるととても学びになったのでオススメです。
「Go言語による並行処理」のサンプルコードはここで見られます。
https://github.com/kat-co/concurrency-in-go-src
サンプルコードの一部に作りかけのまんまで動かないコードがあったのでPR作ってみました。
全然更新してないので取り込まれないだろうな...
https://github.com/kat-co/concurrency-in-go-src/pull/5