こんにちは。KOUKIです。
とあるWeb系企業で、Goエンジニアをやっています。
今日は、goのsync.Lockerを使って、変数管理の方法を学びましょう。
<目次>
シチュエーション
口座か何かの入出金処理を考えてみましょう。
いく人かのグループが以下の図のように共通口座のお金をやりとりします。

そして、最終的な結果(TotalAccounts)は、処理の初期値と変わらないようにしたい、そんなプログラムを実装したいと思います。
一見簡単そうに見えますが、変数(ここではTotalAccounts)の排他制御が必要になるので、少し難しいかもしれません。
sync.Locker
sync.Lockerは、変数の排他制御を提供する便利機能です。
以下のインターフェースを提供しています。
1 2 3 4 |
type Locker interface { Lock() Unlock() } |
同じく排他制御機能を提供するsync.Mutexもあるので、合わせて覚えておいてください。
1 2 3 4 5 6 |
type Mutex struct { // contains filtered or unexported fields } func (*Mutex) Lock func (*Mutex) Unlock |
Mutexには、Lock/Unlockメソッドが提供されています。しかし、sync.Lockerには提供されていないので、自分で実装する必要がありますね。
実装
プロジェクトの作成
以下のファイルを用意してください。
1 2 |
touch multi_ledger.go touch spin_lock.go |
spin_lock.go
先ほどお伝えしたとおり、sync.LockerのLock/Unlockメソッドを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// spin_lock.go package main import ( "runtime" "sync" "sync/atomic" ) type SpinLock int32 // Locker Interfaceを戻り値とする func NewSpinLock() sync.Locker { var lock SpinLock return &lock } // Lockerインターフェース用件を満たす func (s *SpinLock) Lock() { // 数値の入れ替え for !atomic.CompareAndSwapInt32((*int32)(s), 0, 1) { runtime.Gosched() } } // Lockerインターフェース用件を満たす func (s *SpinLock) Unlock() { atomic.StoreInt32((*int32)(s), 0) } |
golangでは、Interfaceに定義した関数(ここではLock/Unlock)をメソッドとして定義するとInterfaceを実装したことになります。
multi_ledger.go
まず、必要なパッケージを読み込みましょう。
1 2 3 4 5 6 7 8 9 10 11 |
// multi_ledger.go package main import ( "math/rand" "sort" "sync" "sync/atomic" "time" ) |
次に、固定値の定義です。
1 2 3 4 5 6 |
const ( totalAccounts = 50000 maxAmountMoved = 10 initialMoney = 100 threads = 4 ) |
accountの貸し借り処理を実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
func peform_movements( ledger *[totalAccounts]int32, locks *[totalAccounts]sync.Locker, totalTrans *int64, ) { for { accountA := rand.Intn(totalAccounts) accountB := rand.Intn(totalAccounts) for accountA == accountB { accountB = rand.Intn(totalAccounts) } amountToMove := rand.Int31n(maxAmountMoved) toLock := []int{accountA, accountB} sort.Ints(toLock) locks[toLock[0]].Lock() locks[toLock[1]].Lock() atomic.AddInt32(&ledger[accountA], -amountToMove) atomic.AddInt32(&ledger[accountB], amountToMove) atomic.AddInt64(totalTrans, 1) locks[toLock[1]].Unlock() locks[toLock[0]].Unlock() } } |
上記のLock、Unlockが肝です。これにより排他制御を行なっています。
rand.Intn
は、指定した数値内の値をランダムで返却します。
最後にmain関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
func main() { println("Total accounts:", totalAccounts, " total threads:", threads, "using SpinLocks") // println("Total accounts:", totalAccounts, " total threads:", threads, "using Mutexes") var ledger [totalAccounts]int32 var locks [totalAccounts]sync.Locker var totalTrans int64 for i := 0; i < totalAccounts; i++ { ledger[i] = initialMoney locks[i] = NewSpinLock() // locks[i] = &sync.Mutex{} } for i := 0; i < threads; i++ { go peform_movements(&ledger, &locks, &totalTrans) } for { time.Sleep(2000 * time.Millisecond) var sum int32 for i := 0; i < totalAccounts; i++ { locks[i].Lock() } for i := 0; i < totalAccounts; i++ { sum += ledger[i] } for i := 0; i < totalAccounts; i++ { locks[i].Unlock() } println(totalTrans, sum) } } |
コメントにMutexバージョンも記載しておきました。Lockerと切り替えられるようになっています。
前述の通り、MutexもLockerもLock/Unlockメソッドを実装しているので、「locks *[totalAccounts]sync.Locker」の引数として渡すことができます(Interface要件を満たすため)。
プログラムを実行してみましょう。
1 2 3 4 5 6 |
total accounts: 50000 total threads: 4 using SpinLocks 7606417 5000000 14569046 5000000 20829923 5000000 28531674 5000000 35257609 5000000 |
いい感じですね。
それでは、Lock/Unlock関数の呼び出しをコメントアウトしましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
func peform_movements( ledger *[totalAccounts]int32, locks *[totalAccounts]sync.Locker, totalTrans *int64, ) { for { accountA := rand.Intn(totalAccounts) accountB := rand.Intn(totalAccounts) for accountA == accountB { accountB = rand.Intn(totalAccounts) } amountToMove := rand.Int31n(maxAmountMoved) toLock := []int{accountA, accountB} sort.Ints(toLock) // locks[toLock[0]].Lock() // locks[toLock[1]].Lock() atomic.AddInt32(&ledger[accountA], -amountToMove) atomic.AddInt32(&ledger[accountB], amountToMove) atomic.AddInt64(totalTrans, 1) // locks[toLock[1]].Unlock() // locks[toLock[0]].Unlock() } } |
プログラムを実行します。
1 2 3 4 5 6 7 |
$ go run *.go Total accounts: 50000 total threads: 4 using SpinLocks 8838907 4999983 17786223 4999885 26794505 5000020 35074637 5000001 42560138 5000020 |
値がバラけてしまいましたね。排他制御が実装できていることになります。
おわりに
排他制御は実務でも重要になるものです。
そのため、MutexやLockerを利用する頻度は高いでしょう。
個人的にはMutexしか使っていませんが^^;
それでは、また!
Go記事まとめ
ソースコード
spin_lock.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// spin_lock.go package main import ( "runtime" "sync" "sync/atomic" ) type SpinLock int32 // Locker Interfaceを戻り値とする func NewSpinLock() sync.Locker { var lock SpinLock return &lock } // Lockerインターフェース用件を満たす func (s *SpinLock) Lock() { // 数値の入れ替え for !atomic.CompareAndSwapInt32((*int32)(s), 0, 1) { runtime.Gosched() } } // Lockerインターフェース用件を満たす func (s *SpinLock) Unlock() { atomic.StoreInt32((*int32)(s), 0) } |
multi_ledger.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
// multi_ledger.go package main import ( "math/rand" "sort" "sync" "sync/atomic" "time" ) const ( totalAccounts = 50000 maxAmountMoved = 10 initialMoney = 100 threads = 4 ) func peform_movements( ledger *[totalAccounts]int32, locks *[totalAccounts]sync.Locker, totalTrans *int64, ) { for { accountA := rand.Intn(totalAccounts) accountB := rand.Intn(totalAccounts) for accountA == accountB { accountB = rand.Intn(totalAccounts) } amountToMove := rand.Int31n(maxAmountMoved) toLock := []int{accountA, accountB} sort.Ints(toLock) // locks[toLock[0]].Lock() // locks[toLock[1]].Lock() atomic.AddInt32(&ledger[accountA], -amountToMove) atomic.AddInt32(&ledger[accountB], amountToMove) atomic.AddInt64(totalTrans, 1) // locks[toLock[1]].Unlock() // locks[toLock[0]].Unlock() } } func main() { println("Total accounts:", totalAccounts, " total threads:", threads, "using SpinLocks") // println("Total accounts:", totalAccounts, " total threads:", threads, "using Mutexes") var ledger [totalAccounts]int32 var locks [totalAccounts]sync.Locker var totalTrans int64 for i := 0; i < totalAccounts; i++ { ledger[i] = initialMoney locks[i] = NewSpinLock() // locks[i] = &sync.Mutex{} } for i := 0; i < threads; i++ { go peform_movements(&ledger, &locks, &totalTrans) } for { time.Sleep(2000 * time.Millisecond) var sum int32 for i := 0; i < totalAccounts; i++ { locks[i].Lock() } for i := 0; i < totalAccounts; i++ { sum += ledger[i] } for i := 0; i < totalAccounts; i++ { locks[i].Unlock() } println(totalTrans, sum) } } |
コメントを残す
コメントを投稿するにはログインしてください。