Go言語学習その35~Goroutineその6:排他制御

前回 ( https://note.com/yasmizohawks/n/ncaab0a36736c ) に引き続き
Go における並行処理を実現する Goroutine を見てみます。

■ 排他制御

複数の関数がそれぞれ別の Goroutine 上で動くとき、
各関数が同じ変数を更新するといった場合は排他制御が必要になります。

以下に排他制御無しの場合のサンプルを示します。

package main

import (
	"fmt"
	"time"
	"sync"
)

var sampleStr string

func funcA(wg *sync.WaitGroup) {
	defer wg.Done()

	sampleStr = "★funcAから設定した文字列"
	time.Sleep(10 * time.Millisecond)
	fmt.Println("■ funcA:sampleStr=", sampleStr)
}

func funcB(wg *sync.WaitGroup) {
	defer wg.Done()

	sampleStr = "★funcBから設定した文字列"
	time.Sleep(10 * time.Millisecond)
	fmt.Println("■ funcB:sampleStr=", sampleStr)
}

func main() {
	var wg sync.WaitGroup

	wg.Add(2)

	go funcA(&wg)
	go funcB(&wg)

	wg.Wait()
	fmt.Println("■ main:終了します")
}

サンプルの内容は、関数の外側に定義されている文字列型変数

var sampleStr string

に対して、funcA 関数、funcB 関数それぞれで値を設定しています。

実行結果は、実行する度に変わりますが以下のようになります。

■ funcB:sampleStr= ★funcAから設定した文字列
■ funcA:sampleStr= ★funcAから設定した文字列
■ main:終了します

実行結果の1行目を見るとfuncB 関数から以下が出力されてますが

■ funcB:sampleStr= ★funcAから設定した文字列

funcB 関数の処理を見ると

sampleStr = "★funcBから設定した文字列"
time.Sleep(10 * time.Millisecond)
fmt.Println("■ funcB:sampleStr=", sampleStr)

sampleStr に「★funcBから設定した文字列」を入れてるので

■ funcB:sampleStr= ★funcBから設定した文字列

と出て欲しいですよね。

ですが、「time.Sleep(10 * time.Millisecond)」で少し待機してるときに
funcA 関数が動いて sampleStr の内容を
「★funcAから設定した文字列」に変えてしまったために

■ funcB:sampleStr= ★funcAから設定した文字列

と出力されたわけです。
排他制御をしていないので、このように別の Goroutine 上の関数の処理が
別の関数の処理に影響を与えてしまいます。

では、上記サンプルに排他制御を入れたものを以下に示します。

package main

import (
	"fmt"
	"time"
	"sync"
)

var sampleStr string

func funcA(wg *sync.WaitGroup, mu *sync.Mutex) {
	defer wg.Done()

	mu.Lock()

	sampleStr = "★funcAから設定した文字列"
	time.Sleep(10 * time.Millisecond)
	fmt.Println("■ funcA:sampleStr=", sampleStr)

	mu.Unlock()
}

func funcB(wg *sync.WaitGroup, mu *sync.Mutex) {
	defer wg.Done()

	mu.Lock()

	sampleStr = "★funcBから設定した文字列"
	time.Sleep(10 * time.Millisecond)
	fmt.Println("■ funcB:sampleStr=", sampleStr)

	mu.Unlock()
}

func main() {
	var wg sync.WaitGroup
	var mu sync.Mutex

	wg.Add(2)

	go funcA(&wg, &mu)
	go funcB(&wg, &mu)

	wg.Wait()
	fmt.Println("■ main:終了します")
}

main 関数で以下の変数宣言をしています。

var mu sync.Mutex

Mutex は排他制御を行うための構造体で、
以下のメソッドが定義されてます。

Lock・・・排他制御したい処理の開始時に実行するメソッド
Unlock・・・排他制御したい処理の終了時に実行するメソッド

Mutex 型の変数 mu を funcA 、funcB 関数の引数に渡して実行していて

go funcA(&wg, &mu)
go funcB(&wg, &mu)

funcA 、funcB 関数では以下の処理を記述してます。

※ funcB の場合は以下の「funcA」部分が「funcB」になる
mu.Lock()
sampleStr = "★funcAから設定した文字列"
time.Sleep(10 * time.Millisecond)
fmt.Println("■ funcA:sampleStr=", sampleStr)
mu.Unlock()

funcA 関数で mu.Lock()を実行するとき、
funcB 関数が先に mu.Lock()を実行済みなら
funcA 関数の mu.Lock() は「ロック解除待ち」になり、
mu.Lock() の次行以降の処理は実行されません。

そして、funcB 関数で mu.Unlock() が実行されたら
funcA 関数の mu.Lock() の次行以降の処理が開始されます。

排他制御の入ったサンプルの実行結果は以下になります。

■ funcB:sampleStr= ★funcBから設定した文字列
■ funcA:sampleStr= ★funcAから設定した文字列
■ main:終了します

排他制御無しのサンプルでは

■ funcB:sampleStr= ★funcAから設定した文字列

となっていた部分が

■ funcB:sampleStr= ★funcBから設定した文字列

となってますね。

sampleStr 変数への値の設定と
「■ funcB:sampleStr=~」の出力を
mu.Lock()~mu.Unlock()の間で行っているために
funcB での上記処理中に
funcA 関数で sampleStr 変数に値が設定されてしまうことが
防げたわけですね。

#プログラミング
#IT
#プログラミング言語
#Go言語
#GO
#Golang

いいなと思ったら応援しよう!