1. ホーム
  2. スクリプト・コラム
  3. ゴラン

Go言語並行プログラミングのための相互排除ロックMutexと読み取り/書き込みロックRWMutex

2022-02-14 02:23:38

並行プログラミングでは、複数の Goroutine Go言語には多くの同期ツールがありますが、今回は相互排他ロックについて紹介します Mut ex と読み取り/書き込みロック RWMutex を使用することができます。

I. 相互排他ロック Mutex

1. ミューテックス入門

Go言語の同期ツールは、主に以下のもので構成されています。 sync パッケージ、および相互排他的ロック ( Mutex ) と読み取り/書き込みロック ( RWMutex ) は、sync パッケージに含まれるメソッドです。

ミューテックスロックは、クリティカルゾーンを保護するために使用することができます。 goroutine が同時にクリティカルゾーンに存在することになります。主な操作はロック(Lockメソッド)とアンロック(Unlockメソッド)の2つで、最初に goroutine クリティカルゾーンに入るとロックされ、出るとアンロックされる。

ミューテックスロック(Mutex)を使用する場合は、以下のことに注意してください。

  • 繰り返しミューテックスをロックしないでください。ブロックされ、またデッドロックになる可能性があります( deadlock ).
  • 相互排他的ロックを解除することで、重複ロックを回避する役割もあります。
  • アンロックまたはアンロックされたミューテックスロックをアンロックしないようにすること。
  • 複数の関数間でミューテックスロックを直接渡さないよう sync.Mutex 型は値型であり、関数に渡すとそのコピーが作成され、関数内のロックに対する操作は元のロックに影響を与えません。

要するに、ミューテックスロックはクリティカルな領域を保護するためにのみ使用され、ロック後は忘れずにロックを解除し、各ロック操作に対して、対応するアンロック操作は1つだけでなければならず、つまり、ロックとアンロックは対で行われなければならず、最も安全な方法は、以下の通りです。 defer ステートメントを使用してロックを解除します。

2. Mutexの使用例

次のコードは、出金と入金操作をシミュレートしたものです。

package main

import (
 "flag"
 "fmt"
 "sync"
)

var (
    mutex sync.Mutex
    balance int
    protecting uint // whether to lock or not
    sign = make(chan struct{}, 10) // channel to wait for all goroutines
)

// deposit money
func deposit(value int) {
    defer func() {
        sign <- struct{}{}
    }()

    if protecting == 1 {
        mutex.Lock()
        defer mutex.Unlock()
    }

    fmt.Printf("Balance: %d\n", balance)
    balance += value
    fmt.Printf("Balance after saving %d: %d\n", value, balance)
    fmt.Println()

}

// Withdraw money
func withdraw(value int) {
    defer func() {
        sign <- struct{}{}
    }()
    
    if protecting == 1 {
        mutex.Lock()
        defer mutex.Unlock()
    }

    fmt.Printf("Balance: %d\n", balance)
    balance -= value
    fmt.Printf("Balance after taking %d: %d\n", value, balance)
    fmt.Println()

}

func main() {
    
    for i:=0; i < 5; i++ {
        go withdraw(500) // take 500
        go deposit(500) // deposit 500
    }

    for i := 0; i < 10; i++ {
  <-sign
 }
    fmt.Printf("Current balance: %d\n", balance)
}

func init() {
    balance = 1000 // initial account balance is 1000
    flag.UintVar(&protecting, "protecting", 0, "whether to lock, 0 means no lock, 1 means lock")
}



上記のコードでは、チャンネルはメインの goroutine を待機させ、他の goroutine の実行が終了すると、各サブ goroutine は実行の終了前にチャンネルに要素を送信し、メインの goroutine は、最後にこのチャネルから要素を受け取ります。 goroutine が同じ回数だけ表示されます。受信後、メインの goroutine .

このコードでは、並行スレッドを使用して、口座への複数(5)の入出金を実装しています。 まず、ロックがない場合を見てみましょう。

Balance: 1000
Balance after 500 deposits: 1500

Balance: 1000
Balance after 500 withdrawals: 1000

Balance: 1000
Balance after 500 deposits: 1500

Balance: 1000
Balance after 500 withdrawals: 1000

Balance: 1000
Balance after 500 deposits: 1500

Balance: 1000
Balance after 500 withdrawals: 1000

Balance: 1000
Balance after 500 withdrawals: 500

Balance: 1000
Balance after 500 deposits: 1000

Balance: 1000
Balance after 500 withdrawals: 500

Balance: 1000
Balance after 500 deposit: 1000

Current balance: 1000



例えば2回目に1000の残高が500を取った後も1000になっているなど、混乱があることがわかりますが、この同じリソースの奪い合いがレースコンディションとして現れるのです( Race Condition ).

ロックの実装結果はこちらです。

Balance: 1000
Balance after taking 500: 500

Balance: 500
Balance after 500 deposit: 1000

Balance: 1000
Balance after 500 withdrawals: 500

Balance: 500
Balance after 500 deposits: 1000

Balance: 1000
Balance after 500 withdrawals: 500

Balance: 500
Balance after 500 deposits: 1000

Balance: 1000
Balance after 500 withdrawals: 1500

Balance: 1500
Balance after 500 withdrawals: 1000

Balance: 1000
Balance after 500 withdrawals: 500

Balance: 500
Balance after 500 deposits: 1000

Current balance: 1000



ロックを追加した後は正常に動作します。

より詳細な相互排他的ロックについては、以下で説明します。 リード/ライト・ミュテックス・ロック RWMutex。

II. リード/ライトロック RWMutex

1. RWMutexの紹介

リード/ライト・ミュテックス・ロック RWMutex 共有リソースの読み取り操作と書き込み操作をそれぞれ保護するための読み取りロックと書き込みロックが含まれています。 sync.RWMutex タイプのLockメソッド Unlock メソッドは、それぞれ書き込みロックとロック解除に使用され、その RLock メソッドと RUnlock メソッドは、それぞれ読み取りロックとロック解除に使用されます。

MutexロックMutexがあるのに、なぜリードロックとライトロックが必要なのでしょうか?なぜなら、多くの同時処理において、同時読み出しが大きな割合を占め、書き込みが比較的少ない場合、読み取りロックと書き込みロックによって同時読み出しが可能になり、サービスパフォーマンスが得られるからです。 リード・ライト・ロックには、次のような特徴があります。

<テーブル 読み取りロックと書き込みロック リードロック 書き込みロック 読み取りロック はい いいえ 書き込みロック いいえ いいえ

つまり

  • 共有リソースが読み込みロックと書き込みロックで保護されている場合、他の goroutine は書き込み操作を行えません。つまり、読み書き操作は並行して行うことができない、つまり互いに排他的である。
  • リードロックで保護されている場合、複数のリード操作を同時に実行することができます。

リード/ライト・ロックを使用する場合、次の点にも注意が必要です。

  • ロックされていない読み書きロックは解除しないでください。
  • リードロックに対してライトロックでアンロックしない
  • リードロックでライトロックを解除することはできません

2. RWMutexの使用例

これまでの出金・入金操作を書き換え、残高を照会する方法を追加します。

package main

import (
 "fmt"
 "sync"
)

// account represents a counter.
type account struct {
 num uint // number of operations
 balance int // balance
 rwMu *sync.RWMutex // read/write lock
RWMutex // Read and write locks }

var sign = make(chan struct{}, 15) // channel to wait for all goroutines

// Check the balance: use read locks
func (c *account) check() {
 defer func() {
        sign <- struct{}{}
    }()
 c.rwMu.RLock()
 defer c.rwMu.RUnlock()
 fmt.Printf("Balance after %d operations: %d\n", c.num, c.balance)
}

// Deposit money: write lock
func (c *account) deposit(value int) {
 defer func() {
        sign <- struct{}{}
    }()
    c.rwMu.Lock()
 defer c.rwMu.Unlock() 

 fmt.Printf("Balance: %d\n", c.balance)   
 c.num += 1
    c.balance += value
    fmt.Printf("Balance after saving %d: %d\n", value, c.balance)
    fmt.Println() 
}

// Take the money: write the lock
func (c *account) withdraw(value int) {
    defer func() {
        sign <- struct{}{}
    }()
 c.rwMu.Lock()
 defer c.rwMu.Unlock()   
 fmt.Printf("Balance: %d\n", c.balance)     
 c.num += 1
    c.balance -= value
 fmt.Printf("Balance after taking %d: %d\n", value, c.balance)
    fmt.Println()  
}


func main() {
 c := account{0, 1000, new(sync.RWMutex)}

 for i:= 0; i < 5; i++ {
        go c.withdraw(500) // take 500
        go c.deposit(500) // save 500
  go c.check()
    }

    for i := 0; i < 15; i++ {
  <-sign
 }
 fmt.Printf("Balance after %d operations: %d\n", c.num, c.balance)

}



実装結果です。

Balance: 1000
Balance after 500: 500

Balance after 1 operation: 500
Balance after 1 operation: 500
Balance after 1 operation: 500
Balance after 1 operation: 500
Balance after 1 operation: 500
Balance: 500
Balance after 500 deposits: 1000

Balance: 1000
Balance after 500 withdrawals: 500

Balance: 500
Balance after 500 deposits: 1000

Balance: 1000
Balance after 500 withdrawals: 1500

Balance: 1500
Balance after 500 withdrawals: 1000

Balance: 1000
Balance after 500 withdrawals: 500

Balance: 500
Balance after 500 deposits: 1000

Balance: 1000
Balance after 500 withdrawals: 500

Balance: 500
Balance after 500 deposits: 1000

Balance after 10 operations: 1000



read and writeロックとmutexロックの違いは、read and writeロックは共有リソースに対する読み取りと書き込みの操作を分離し、より高度なアクセス制御を可能にすることです。

要約すると

リードライトロックもミューテックスロックの一種で、ミューテックスロックを拡張したものです。 使用する際は、以下の点に注意が必要です。

  • ロックを追加した後は必ずロックを解除する
  • ロックとアンロックを繰り返さない
  • 無施錠のロックを解除しない
  • 相互に排他的なロックは渡さない

Go言語並行プログラミングMutexとRWMutexの記事はこれだけです。Go言語のMutexとRWMutexについては、過去の記事を検索するか、以下の記事を引き続き閲覧してください。