2013年7月2日火曜日

Go言語 排他処理

こんにちは。scarvizです。

今回は排他処理(Mutex)の紹介です。



■排他処理
排他処理にはMutexを使用します。
sync.MutexのLockメソッドでロック、Unlockメソッドでロックを解除します。

import "sync"

var lock sync.Mutex    // ロックオブジェクト
lock.Lock()            // ロックする
...                    // 排他対象の処理
lock.Unlock()          // ロックを解除する


■ロックの確認
ゴルーチンを使ってロックの確認をしてみます。
試しに以下を実施します。

package main

import (
 "fmt"
 "sync"
)

func main() {
 mes := "メッセージ1"
 var lock sync.Mutex
 go func() {
  lock.Lock()
  mes = "メッセージ2"
  fmt.Println("Goroutine:" + mes)
  lock.Unlock()
 }()

 lock.Lock()
 fmt.Println(mes)
 lock.Unlock()
}
結果:
メッセージ1


これはゴルーチンが実行される前にmain関数が終了しているためです。
ロックされるのを確認するには、以下のようにしてみます。

package main

import (
 "fmt"
 "sync"
)

func main() {
 mes := "メッセージ1"
 var lock sync.Mutex
 lock.Lock() // lockはUnlockされるまでロックする
 go func() {
  mes = "メッセージ2"
  fmt.Println("Goroutine:" + mes)
  lock.Unlock()
 }()

 lock.Lock() // lockを使ってロックしている場合はUnlockされるまでここで待つ
 fmt.Println(mes)
 lock.Unlock()
}
結果:
Goroutine:メッセージ2
メッセージ2


今度はLockメソッドでロックされているのが確認できましたね。
Goではチャネルというスレッド間通信で同期処理を行うことが出来るので、ゴルーチンではチャネルで、ログファイルやDBへの入出力などの排他処理にMutexを使用すれば良いかなと思います。


■Mutexとチャネル
個人的な意見ですが、チャネルが良いと思える理由を少し挙げてみたいと思います。
例えば何かスレッド上で処理を行い、進捗度を表示し、キャンセル処理のための入力待ちもするプログラムを作成するとします。
Mutexで進捗度と停止フラグを排他した場合と、進捗度と停止フラグをチャネルで通信した場合とを比較してみました。


まずはMutexを使った場合です。

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

func mutexSample() {
 // 進捗度
 progress := 0
 var lockProgress sync.Mutex
 setProgress := func(val int) {
  lockProgress.Lock()
  progress = val
  lockProgress.Unlock()
 }
 getProgress := func() int {
  lockProgress.Lock()
  ret := progress
  lockProgress.Unlock()

  return ret
 }

 // 停止フラグ
 stop := false
 var lockStop sync.Mutex
 setStop := func(val bool) {
  lockStop.Lock()
  stop = val
  lockStop.Unlock()
 }
 getStop := func() bool {
  lockStop.Lock()
  ret := stop
  lockStop.Unlock()

  return ret
 }

 //  処理を行って進捗度を更新する
 go func() {
  v := 100
  fmt.Println("mutexSample start")
  for {
   time.Sleep(time.Second * 1)
   v = v - 5
   setProgress(100 - v)
  }
 }()

 // 途中で中止する用の入力待ち処理
 go func() {
  var input string
  fmt.Println("処理を中止するには何かキー入力して、Enterキーを押してください")
  fmt.Println("0                  50                  100")
  fmt.Scan(&input)
  setStop(true)
  fmt.Println("stop")
 }()

 backVal := 0
 // 進捗・停止処理ループ
 for {
  time.Sleep(time.Millisecond * 1)

  // 停止フラグが立っていた場合はループを抜ける
  if getStop() {
   break
  }

  val := getProgress()
  // 進捗が100%以上になっていたらループを抜ける
  if val >= 100 {
   fmt.Print("■ ")
   fmt.Println("\n完了しました")
   break
  } else {
   if backVal < val {
    fmt.Print("■ ")
    backVal = val
   }
   continue
  }
 }
}


次にチャネルを使った場合です。

import (
 "fmt"
 "time"
)

func goroutineSample() {
 ch := make(chan int)
 val := 100

 // 処理を行って進捗度をチャネルで渡す
 go func(s chan int, v int) {
  fmt.Println("goroutine sample start")
  for {
   time.Sleep(time.Second * 1)
   v = v - 5
   s <- 100 - v
  }
 }(ch, val)

 stop := make(chan bool)

 // 途中で中止する用の入力待ち処理
 go func(s chan bool) {
  var input string
  fmt.Println("処理を中止するには何かキー入力して、Enterキーを押してください")
  fmt.Println("0                  50                  100")
  fmt.Scan(&input)
  s <- true
 }(stop)

 // 進捗・停止処理ループ
 for {
  select {
  // 進捗チャネルを受信した場合は進捗度を表示
  case progress := <-ch:
   fmt.Print("■ ")
   
   // 進捗が100%以上になっていたら処理を終了する
   if progress >= 100 {
    fmt.Println("\n完了しました")
    return
   }
  // 停止チャネルを受信した場合は処理を終了する
  case <-stop:
   fmt.Println("stop")
   return
  // 上記以外の場合1ミリ秒待ってループ処理を続行する
  default:
   time.Sleep(time.Millisecond * 1)
  }
 }
}


若干動きが違うところはありますが、だいたい同じ動きをするようになっています。 
Mutexでは共有メモリに対してset,getの関数をその数だけ用意していますが、チャネルでは不要になります。
また、チャネルは受信時に受信するまでそこで待機しますが、selectを使用することで未受信時の処理を行ったり、他のチャネルの受信処理を行ったり出来ます。
これらのことで、チャネルを使った場合では、コード量が減り、スレッド内の処理は排他を考えなくても良くなります。
他にも恩恵は色々あると思いますが、排他処理の設計ミスや実装ミスで苦労することはなくなると思うので、随分楽になりますね。

0 件のコメント:

コメントを投稿