なぜロックが必要なのか
ロックは同期パッケージの中核であり、Lock と Unlock という 2 つの主要なメソッドがあります。
同時実行の場合、複数のスレッドまたはコルーチンが同時に変数を変更します。ロックを使用すると、一定期間内に 1 つのコルーチンまたはスレッドのみがこの変数を変更するようにできます。
ロックを使用しない場合、以下に示すように、同時状況では望ましい結果が得られない可能性があります。
package main
import (
"fmt"
"time"
)
func main() {
var a = 0
for i := 0; i < 1000; i++ {
go func(idx int) {
a += 1
fmt.Println(a)
}(i)
}
time.Sleep(time.Second)
}
上記のプログラムは理論的にはaの値を順次インクリメントして出力することになりますが、実際の結果は以下のようになります。
537
995
996
997
538
999
1000
実行結果から、a の値が順番に増加していないことがわかりますが、これはなぜでしょうか?
コルーチンの実行順序はおおよそ次のとおりです。
- レジスタから a の値を読み取ります。
- 次に、加算演算を実行します。
- 最後にレジスタに書き込みます。
上記のシーケンスによれば、 a の値を 3 として取得して加算演算を実行するコルーチンがあった場合、別のコルーチンが a の値を取得し、取得された値も 3 となり、最終的に結果が返されます。 2 つのコルーチンは同じです。
ロックの概念は、コルーチンが a を処理しているときに a をロックし、他のコルーチンはコルーチンの処理が完了して a のロックを解除してから続行する必要があるということです。 1 つを持っているため、上記の例の状況は回避されます。
ミューテックス
上記の例の問題を解決するにはどうすればよいでしょうか?相互排他ロック Mutex を追加します。では、ミューテックスとは何でしょうか?ミューテックスには、次のように呼び出すことができるメソッドが 2 つあります。
func (m *Mutex) Lock()
func (m *Mutex) Unlock()
上記のコードを次のように少し変更します。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var a = 0
var lock sync.Mutex
for i := 0; i < 1000; i++ {
go func(idx int) {
lock.Lock()
defer lock.Unlock()
a += 1
fmt.Printf("goroutine %d, a=%d\n", idx, a)
}(i)
}
// 1秒待ってプログラムを終了させる
// すべてのgoroutineが実行されることを確認する
time.Sleep(time.Second)
}
操作の結果は次のようになります。
goroutine 995, a=996
goroutine 996, a=997
goroutine 997, a=998
goroutine 998, a=999
goroutine 999, a=1000
ミューテックスは同時に 1 つの goroutine によってのみロックでき、他の goroutine はミューテックスのロックが解除されるまでブロックされることに注意してください (ミューテックスのロックを再競合します)。サンプル コードは次のとおりです。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
ch := make(chan struct{}, 2)
var l sync.Mutex
go func() {
l.Lock()
defer l.Unlock()
fmt.Println("goroutine1: 私は2秒ぐらいロックするでしょう")
time.Sleep(time.Second * 2)
fmt.Println("goroutine1: ロック解除しました、皆さんどうぞ!")
ch <- struct{}{}
}()
go func() {
fmt.Println("goroutine2: ロック解除を待っています")
l.Lock()
defer l.Unlock()
fmt.Println("goroutine2: やったー、私もロック解除しました")
ch <- struct{}{}
}()
// goroutineが終了するまで待機
for i := 0; i < 2; i++ {
<-ch
}
}
![読み書きロック](https://it-kiso.com/wp-content/uploads/2023/05/title_image_template33-2.png)
読み取り/書き込みロックには次の 4 つの方法があります。
- 書き込み操作のロックとロック解除は、それぞれ
func (*RWMutex) Lock
とfunc (*RWMutex) Unlock
です。
- 読み取り操作のロックとロック解除は、それぞれ
func (*RWMutex) Rlock
とfunc (*RWMutex) RUnlock
です。
読み取り/書き込みロックの違いは次のとおりです。
- ゴルーチンが書き込みロックを取得すると、他のゴルーチンは、読み取りロックであっても書き込みロックであっても、書き込みのロックが解除されるまでブロックされます。
- goroutine が読み取りロックを取得しても、他の読み取りロックは引き続き継続できます。
- 1 つ以上の読み取りロックがある場合、書き込みロックは、すべての読み取りロックがロック解除されるまで待機してから、書き込みロックが可能になります。
したがって、ここでの読み取りロック (RLock) の目的は、実際には、多くのコルーチンまたはプロセスがデータを読み取り中であること、および書き込み操作は書き込み (書き込みロック) 前に読み取り (読み取りロックの解除) を待つ必要があることを書き込みロックに伝えることです。
それは次の 3 行に要約できます。
- 同時に書き込みロックを取得できるゴルーチンは 1 つだけです。
- 同時に、任意の数のゴルーインテが読み取りロックを取得できます。
- 同時に存在できるのは書き込みロックまたは読み取りロックのみです (読み取りと書き込みは相互に排他的です)。
サンプルコードは次のとおりです。
package main
import (
"fmt"
"math/rand"
"sync"
)
var count int
var rw sync.RWMutex
func main() {
ch := make(chan struct{}, 10)
for i := 0; i < 5; i++ {
go read(i, ch)
}
for i := 0; i < 5; i++ {
go write(i, ch)
}
for i := 0; i < 10; i++ {
<-ch
}
}
func read(n int, ch chan struct{}) {
rw.RLock()
fmt.Printf("goroutine %d 操作...\n", n)
v := count
fmt.Printf("goroutine %d ,値は:%d\n", n, v)
rw.RUnlock()
ch <- struct{}{}
}
func write(n int, ch chan struct{}) {
rw.Lock()
fmt.Printf("goroutine %d 操作...\n", n)
v := rand.Intn(1000)
count = v
fmt.Printf("goroutine %d 書き込み終了、新しい値は:%d\n", n, v)
rw.Unlock()
ch <- struct{}{}
}
その実行結果は次のとおりです。
goroutine 0 操作…
goroutine 0 值:0
goroutine 3 操作…
goroutine 1 操作…
goroutine 3 值:0
goroutine 1 值:0
goroutine 4 操作…
goroutine 4 值:81
goroutine 4 操作…
goroutine 4 值:81
goroutine 2 操作…
goroutine 2 值:81
goroutine 0 操作…
goroutine 0 值:887
goroutine 1 操作…
goroutine 1 值:847
goroutine 2 操作…
goroutine 2 值:59
goroutine 3 操作…
goroutine 3 值:81
さらに 2 つの例を見てみましょう。
【例1】変数を複数の読み込みで同時に読み込んだ場合、ロックがかかりますが読み込みには影響しません。 (読み取りと書き込みは相互に排他的ですが、読み取りと読み取りは相互排他的ではありません)
package main
import (
"sync"
"time"
)
func m *sync.RWMutex
func main() {
m = new(sync.RWMutex)
//複数の同時読み取り
go read(1)
go read(2)
time.Sleep(2*time.Second)
}
func read(i int) {
println(i,"読み取り開始")
m.RLock()
println(i,"読み取り中")
time.Sleep(1*time.Second)
m.RUnlock()
println(i,"読み取り完了")
}
操作の結果は次のようになります。
1 read start
1 reading
2 read start
2 reading
1 read over
2 read over
[例 2] 読み取りと書き込みは相互に排他的であるため、書き込み操作が開始されると、読み取り操作は書き込み操作が完了するまで待ってから続行する必要があります。そうでない場合、読み取り操作は待ち続けることしかできません。
package main
import (
"sync"
"time"
)
var M *sync.RWMutex
func main() {
m = new(sync.RWMutex)
//書くとき何もできない
go write(1)
go read(2)
go write(3)
time.Sleep(2*time.Second)
}
func read(i int) {
println(i,"読み取り開始")
m.RLock()
println(i,"読み取り中")
time.Sleep(1*time.Second)
m.RUnlock()
println(i,"読み取り完了")
}
func write(i int) {
println(i,"書き込み開始")
m.Lock()
println(i,"書き込み中")
time.Sleep(1*time.Second)
m.Unlock()
println(i,"書き込み完了")
}
操作の結果は次のようになります。
1 write start
3 write start
1 writing
2 read start
1 write over
2 reading