#概要
2度目の投稿です。
Goで排他制御の仕組みを書く機会があったので、記録していきます。
今回は2種類の方法で排他制御の仕組みを作っていきます。
#各種ツール
- go 1.14
- VSCode(1.50.1)
- golang.org/x/sync v0.0.0-20201020160332
#シナリオ
今回はあるテーブルを共有資源として、テーブルに対して複数のプロセスから処理が行われたときに、先行のプロセスが終わるまで、後続のプロセスがテーブルを操作することが出来ないようにする仕組みを作っていきます。
まずは排他制御を行わず、ゴルーチンをつかって同時にテーブルをupdateしてみます。
(sync.WaitGroupはメインのgoroutineを待機させるためのものです。詳しくはこちら。)
var w sync.WaitGroup
func main() {
w.Add(2)
go update()
go update()
w.Wait()
}
func update() {
defer w.Done()
for i := 0; i <= 10; i += 5 {
fmt.Println("tbl update:", i*10, "%")
time.Sleep(time.Second)
}
}
// tbl update: 0 %
// tbl update: 0 %
// tbl update: 50 %
// tbl update: 50 %
// tbl update: 100 %
// tbl update: 100 %
2つのプロセスが同時にテーブルのupdateをしている様子がわかります。
今回はこれを先行のupdateが終わってから、後続のupdateが行われるように、下記のような出力を得られる仕組みを作っていきます。
// tbl update: 0 %
// tbl update: 50 %
// tbl update: 100 %
// tbl update: 0 %
// tbl update: 50 %
// tbl update: 100 %
#sync.Mutexを使ったパターン
まずは、sync.Mutexからです。
// メイン関数は上記と同様
var m sync.Mutex
func update() {
defer w.Done()
defer m.Unlock()
m.Lock()
for i := 0; i <= 10; i += 5 {
fmt.Println("tbl update:", i*10, "%")
time.Sleep(time.Second)
}
}
// tbl update: 0 %
// tbl update: 50 %
// tbl update: 100 %
// tbl update: 0 %
// tbl update: 50 %
// tbl update: 100 %
update()
の冒頭でUnLock()
およびLock()
を宣言します。
後続のゴルーチンは先行のゴルーチンがUnLock()
するまで(関数の処理が終わるまで)Lock()
をかけることが出来ず、結果として先行ゴルーチンの処理の終了を待つことになります。
狙った通りの出力が得られていることが分かります。
sync.Mutexは非常にシンプルで分かりやすいですが、処理を行えるプロセスは一つになります。
今回のシナリオとは少し違いますが、例えば「サーバーへの負荷を抑えるため、1000以上のプロセスが同時に実行しないようにする」などの仕組みを作ることは難しそうです。
#セマフォを使ったパターン
筆者はセマフォを使うのが初めてなので、セマフォがなんなのかってとこから書いていきます。
セマフォとは?
セマフォは排他制御の仕組みの一つで、鉄道線路の運行をモデル化したものです。
(semaphoreは日本語で信号機の意)
線路という共有資源を同時に使うことがないよう制御するために作られました。
セマフォはセマフォ変数、P操作、V操作の3つで成り立ちます。
- セマフォ変数
- 共有資源にアクセスできるプロセスの数
- 負にはならない
- セマフォ変数が0 or 1しかとらないものを2値セマフォ(バイナリセマフォ)、0からNをとるものをゼネラルセマフォと呼ぶ
- P操作
- セマフォ変数をデクリメントする
- あるプロセスが共有資源を確保する
- V操作
- セマフォ変数をインクリメントする
- あるプロセスが共有資源を解放する
今回のシナリオは2値セマフォで、update()
の冒頭でP操作を、終わりにV操作を行う必要がありそうです。
実装
golang.org/x/syncを使って実装します。
const semaphoreCnt = 1
var s *semaphore.Weighted = semaphore.NewWeighted(semaphoreCnt)
func update() {
defer w.Done()
defer s.Release(1) //V操作 セマフォ変数を1増やす
s.Acquire(context.TODO(), 1) //P操作 セマフォ変数を1減らす
for i := 0; i <= 10; i += 5 {
fmt.Println("tbl update:", i*10, "%")
time.Sleep(time.Second)
}
}
// tbl update: 0 %
// tbl update: 50 %
// tbl update: 100 %
// tbl update: 0 %
// tbl update: 50 %
// tbl update: 100 %
golang.org/x/sync
のAcquire()
がP操作に,Release()
がV操作にあたります。
semaphore.Weighted
でセマフォ変数を定義し、Acquire()
及びRelease()
を用いて、増減させることで排他制御を実現しています。
また、golang.org/x/sync
では、Acquire()
,Release()
でセマフォ変数をいくつ増減させるかを調節することが出来ます。(今回は2値セマフォのため、それぞれ1増減となっています。)
これにより、各プロセスの処理の重みを定義することができます。
golang.org/x/sync
を使うことでsync.Mutex
と比べ、より複雑な排他制御の仕組みを作ることができます。
#参考