プログラミング言語 golang go package Go 言語同期パッケージとロック: 変数へのスレッド アクセスを制限する

Go 言語同期パッケージとロック: 変数へのスレッド アクセスを制限する

 
 
Go 言語の sync パッケージは、同時プロセス中に 2 つ以上のコルーチン (またはスレッド) が同時に同じ変数を読み書きする可能性がある状況に対処するために、相互排他ロック Mutex と読み取り/書き込みロック RWMutex を提供します。

 

なぜロックが必要なのか

ロックは同期パッケージの中核であり、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
    }
} 

 

読み書きロック

読み取り/書き込みロックには次の 4 つの方法があります。

  • 書き込み操作のロックとロック解除は、それぞれfunc (*RWMutex) Lockfunc (*RWMutex) Unlockです。
  • 読み取り操作のロックとロック解除は、それぞれfunc (*RWMutex) Rlockfunc (*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

 

「 Go 言語同期パッケージとロック: 変数へのスレッド アクセスを制限する」についてわかりやすく解説!絶対に観るべきベスト2動画

Go言語とは?|プログラミング言語のGo言語について3分でわかりやすく解説します【プログラミング初心者向け】
【Go言語 超入門コース】09.演算子|足し算や引き算、2つの値の大小を比較するときに使います【プログラミング初心者向け入門講座】
 
 
Go 言語の sync パッケージは、同時プロセス中に 2 つ以上のコルーチン (またはスレッド) が同時に同じ変数を読み書きする可能性がある状況に対処するために、相互排他ロック Mutex と読み取り/書き込みロック RWMutex を提供します。

 

なぜロックが必要なのか

ロックは同期パッケージの中核であり、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
    }
} 

 

読み書きロック

読み取り/書き込みロックには次の 4 つの方法があります。

  • 書き込み操作のロックとロック解除は、それぞれfunc (*RWMutex) Lockfunc (*RWMutex) Unlockです。
  • 読み取り操作のロックとロック解除は、それぞれfunc (*RWMutex) Rlockfunc (*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

 

「 Go 言語同期パッケージとロック: 変数へのスレッド アクセスを制限する」についてわかりやすく解説!絶対に観るべきベスト2動画

Go言語とは?|プログラミング言語のGo言語について3分でわかりやすく解説します【プログラミング初心者向け】
【Go言語 超入門コース】09.演算子|足し算や引き算、2つの値の大小を比較するときに使います【プログラミング初心者向け入門講座】